Skip to content

Commit 52a8706

Browse files
authored
Merge pull request #706 from fcollonval/auto-backport-of-pr-630-on-0.11.x
Backport PR #630 UI feedback of Git execution
2 parents cd2f97f + 84aa744 commit 52a8706

29 files changed

+2359
-905
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ jupyter lab build
3232

3333
Once installed, extension behavior can be modified via the following settings which can be set in JupyterLab's advanced settings editor:
3434

35-
- **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.
36-
- **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.
37-
- **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.
38-
- **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.
35+
- **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.
36+
- **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.
37+
- **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.
38+
- **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.
39+
- **doubleClickDiff**: double click a file in the Git extension panel to open a diff of the file instead of opening the file for editing.
40+
- **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.
41+
- **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.
42+
- **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.
3943

4044
### Troubleshooting
4145

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
"@jupyterlab/ui-components": "^1.1.0",
6363
"@material-ui/core": "^4.8.2",
6464
"@material-ui/icons": "^4.5.1",
65+
"@material-ui/lab": "^4.0.0-alpha.54",
66+
"@phosphor/collections": "^1.2.0",
6567
"@phosphor/widgets": "^1.8.0",
6668
"diff-match-patch": "^1.0.4",
6769
"nbdime": "~5.0.1",

schema/plugin.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"description": "jupyterlab-git settings.",
66
"type": "object",
77
"properties": {
8+
"blockWhileCommandExecutes": {
9+
"type": "boolean",
10+
"title": "Suspend user interaction until commands finish",
11+
"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.",
12+
"default": true
13+
},
814
"cancelPullMergeConflict": {
915
"type": "boolean",
1016
"title": "Cancel pull merge conflict",
@@ -17,10 +23,16 @@
1723
"description": "Disable all branch operations (new, switch) when there are changed/staged files",
1824
"default": false
1925
},
26+
"displayStatus": {
27+
"type": "boolean",
28+
"title": "Display Git status updates",
29+
"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.",
30+
"default": true
31+
},
2032
"doubleClickDiff": {
2133
"type": "boolean",
2234
"title": "Show diff on double click",
23-
"description": "If true, doubling clicking a file in the list of changed files will open a diff",
35+
"description": "If true, doubling clicking a file in the list of changed files will open a diff.",
2436
"default": false
2537
},
2638
"historyCount": {

src/gitMenuCommands.ts renamed to src/commandsAndMenu.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,23 @@ import {
99
import { ISettingRegistry } from '@jupyterlab/coreutils';
1010
import { FileBrowser } from '@jupyterlab/filebrowser';
1111
import { ITerminal } from '@jupyterlab/terminal';
12+
import { CommandRegistry } from '@phosphor/commands';
13+
import { Menu } from '@phosphor/widgets';
1214
import { IGitExtension } from './tokens';
15+
import { GitCredentialsForm } from './widgets/CredentialsBox';
1316
import { doGitClone } from './widgets/gitClone';
1417
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';
15-
import { GitCredentialsForm } from './widgets/CredentialsBox';
18+
19+
const RESOURCES = [
20+
{
21+
text: 'Set Up Remotes',
22+
url: 'https://www.atlassian.com/git/tutorials/setting-up-a-repository'
23+
},
24+
{
25+
text: 'Git Documentation',
26+
url: 'https://git-scm.com/doc'
27+
}
28+
];
1629

1730
/**
1831
* The command IDs used by the git plugin.
@@ -210,6 +223,52 @@ export function addCommands(
210223
});
211224
}
212225

226+
/**
227+
* Adds commands and menu items.
228+
*
229+
* @private
230+
* @param app - Jupyter front end
231+
* @param gitExtension - Git extension instance
232+
* @param fileBrowser - file browser instance
233+
* @param settings - extension settings
234+
* @returns menu
235+
*/
236+
export function createGitMenu(commands: CommandRegistry): Menu {
237+
const menu = new Menu({ commands });
238+
menu.title.label = 'Git';
239+
[
240+
CommandIDs.gitInit,
241+
CommandIDs.gitClone,
242+
CommandIDs.gitPush,
243+
CommandIDs.gitPull,
244+
CommandIDs.gitAddRemote,
245+
CommandIDs.gitTerminalCommand
246+
].forEach(command => {
247+
menu.addItem({ command });
248+
});
249+
250+
menu.addItem({ type: 'separator' });
251+
252+
menu.addItem({ command: CommandIDs.gitToggleSimpleStaging });
253+
254+
menu.addItem({ command: CommandIDs.gitToggleDoubleClickDiff });
255+
256+
menu.addItem({ type: 'separator' });
257+
258+
const tutorial = new Menu({ commands });
259+
tutorial.title.label = ' Help ';
260+
RESOURCES.map(args => {
261+
tutorial.addItem({
262+
args,
263+
command: CommandIDs.gitOpenUrl
264+
});
265+
});
266+
267+
menu.addItem({ type: 'submenu', submenu: tutorial });
268+
269+
return menu;
270+
}
271+
213272
/* eslint-disable no-inner-declarations */
214273
namespace Private {
215274
/**

src/components/Alert.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as React from 'react';
2+
import Portal from '@material-ui/core/Portal';
3+
import Snackbar from '@material-ui/core/Snackbar';
4+
import Slide from '@material-ui/core/Slide';
5+
import { default as MuiAlert } from '@material-ui/lab/Alert';
6+
import { Severity } from '../tokens';
7+
8+
/**
9+
* Returns a React component for "sliding-in" an alert.
10+
*
11+
* @private
12+
* @param props - component properties
13+
* @returns React element
14+
*/
15+
function SlideTransition(props: any): React.ReactElement {
16+
return <Slide {...props} direction="up" />;
17+
}
18+
19+
/**
20+
* Interface describing component properties.
21+
*/
22+
export interface IAlertProps {
23+
/**
24+
* Boolean indicating whether to display an alert.
25+
*/
26+
open: boolean;
27+
28+
/**
29+
* Alert message.
30+
*/
31+
message: string;
32+
33+
/**
34+
* Alert severity.
35+
*/
36+
severity?: Severity;
37+
38+
/**
39+
* Alert duration (in milliseconds).
40+
*/
41+
duration?: number;
42+
43+
/**
44+
* Callback invoked upon clicking on an alert.
45+
*/
46+
onClick?: (event?: any) => void;
47+
48+
/**
49+
* Callback invoked upon closing an alert.
50+
*/
51+
onClose: (event?: any) => void;
52+
}
53+
54+
/**
55+
* React component for rendering an alert.
56+
*/
57+
export class Alert extends React.Component<IAlertProps> {
58+
/**
59+
* Returns a React component for rendering an alert.
60+
*
61+
* @param props - component properties
62+
* @returns React component
63+
*/
64+
constructor(props: IAlertProps) {
65+
super(props);
66+
}
67+
68+
/**
69+
* Renders the component.
70+
*
71+
* @returns React element
72+
*/
73+
render(): React.ReactElement {
74+
let duration: number | null = null;
75+
76+
const severity = this.props.severity || 'info';
77+
if (severity === 'success') {
78+
duration = this.props.duration || 5000; // milliseconds
79+
}
80+
return (
81+
<Portal>
82+
<Snackbar
83+
key="git:alert"
84+
open={this.props.open}
85+
anchorOrigin={{
86+
vertical: 'bottom',
87+
horizontal: 'right'
88+
}}
89+
autoHideDuration={duration}
90+
TransitionComponent={SlideTransition}
91+
onClick={this._onClick}
92+
onClose={this._onClose}
93+
>
94+
<MuiAlert variant="filled" severity={severity}>
95+
{this.props.message || '(missing message)'}
96+
</MuiAlert>
97+
</Snackbar>
98+
</Portal>
99+
);
100+
}
101+
102+
/**
103+
* Callback invoked upon clicking on an alert.
104+
*
105+
* @param event - event object
106+
*/
107+
private _onClick = (event: any): void => {
108+
if (this.props.onClick) {
109+
this.props.onClick(event);
110+
return;
111+
}
112+
this._onClose(event, 'click');
113+
};
114+
115+
/**
116+
* Callback invoked upon closing an alert.
117+
*
118+
* @param event - event object
119+
* @param reason - reason why the callback was invoked
120+
*/
121+
private _onClose = (event: any, reason: string): void => {
122+
if (reason === 'clickaway') {
123+
return;
124+
}
125+
this.props.onClose(event);
126+
};
127+
}

0 commit comments

Comments
 (0)