diff --git a/src/components/GitPanel.tsx b/src/components/GitPanel.tsx
index 6cfee14c3..bf81fa6ef 100644
--- a/src/components/GitPanel.tsx
+++ b/src/components/GitPanel.tsx
@@ -1,22 +1,19 @@
import * as React from 'react';
import { showErrorMessage, showDialog } from '@jupyterlab/apputils';
import { ISettingRegistry } from '@jupyterlab/coreutils';
+import { FileBrowserModel } from '@jupyterlab/filebrowser';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { JSONObject } from '@phosphor/coreutils';
import { GitExtension } from '../model';
-import {
- findRepoButtonStyle,
- panelContainerStyle,
- panelWarningStyle
-} from '../style/GitPanelStyle';
+import { panelContainerStyle } from '../style/GitPanelStyle';
import { Git } from '../tokens';
import { decodeStage } from '../utils';
import { GitAuthorForm } from '../widgets/AuthorBox';
import { BranchHeader } from './BranchHeader';
+import { CommitBox } from './CommitBox';
import { FileList } from './FileList';
import { HistorySideBar } from './HistorySideBar';
import { PathHeader } from './PathHeader';
-import { CommitBox } from './CommitBox';
/** Interface for GitPanel component state */
export interface IGitSessionNodeState {
@@ -40,6 +37,7 @@ export interface IGitSessionNodeProps {
model: GitExtension;
renderMime: IRenderMimeRegistry;
settings: ISettingRegistry.ISettings;
+ fileBrowserModel: FileBrowserModel;
}
/** A React component for the git extension's main display */
@@ -199,7 +197,7 @@ export class GitPanel extends React.Component<
render() {
let filelist: React.ReactElement;
- let main: React.ReactElement;
+ let main: React.ReactElement = null;
let sub: React.ReactElement;
let msg: React.ReactElement;
@@ -270,26 +268,13 @@ export class GitPanel extends React.Component<
{sub}
);
- } else {
- main = (
-
-
You aren’t in a git repository.
-
-
- );
}
return (
{
await this.refreshBranch();
if (this.state.isHistoryVisible) {
diff --git a/src/components/PathHeader.tsx b/src/components/PathHeader.tsx
index 1451b194e..aad65bc5c 100644
--- a/src/components/PathHeader.tsx
+++ b/src/components/PathHeader.tsx
@@ -1,117 +1,214 @@
+import * as React from 'react';
import { Dialog, showDialog, UseSignal } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
-import * as React from 'react';
+import { FileBrowserModel, FileDialog } from '@jupyterlab/filebrowser';
+import { DefaultIconReact } from '@jupyterlab/ui-components';
import { classes } from 'typestyle';
import {
gitPullStyle,
gitPushStyle,
+ noRepoPathStyle,
+ pinIconStyle,
repoPathStyle,
+ repoPinStyle,
repoRefreshStyle,
- repoStyle
+ repoStyle,
+ separatorStyle,
+ toolBarStyle
} from '../style/PathHeaderStyle';
+import { IGitExtension } from '../tokens';
import { GitCredentialsForm } from '../widgets/CredentialsBox';
import { GitPullPushDialog, Operation } from '../widgets/gitPushPull';
-import { IGitExtension } from '../tokens';
+/**
+ * Properties of the PathHeader React component
+ */
export interface IPathHeaderProps {
+ /**
+ * Git extension model
+ */
model: IGitExtension;
+ /**
+ * File browser model
+ */
+ fileBrowserModel: FileBrowserModel;
+ /**
+ * Refresh UI callback
+ */
refresh: () => Promise;
}
-export class PathHeader extends React.Component {
- constructor(props: IPathHeaderProps) {
- super(props);
+/**
+ * Displays the error dialog when the Git Push/Pull operation fails.
+ *
+ * @param title the title of the error dialog
+ * @param body the message to be shown in the body of the modal.
+ */
+async function showGitPushPullDialog(
+ model: IGitExtension,
+ operation: Operation
+): Promise {
+ let result = await showDialog({
+ title: `Git ${operation}`,
+ body: new GitPullPushDialog(model, operation),
+ buttons: [Dialog.okButton({ label: 'DISMISS' })]
+ });
+ let retry = false;
+ while (!result.button.accept) {
+ retry = true;
+
+ let response = await showDialog({
+ title: 'Git credentials required',
+ body: new GitCredentialsForm(
+ 'Enter credentials for remote repository',
+ retry ? 'Incorrect username or password.' : ''
+ ),
+ buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
+ });
+
+ if (response.button.accept) {
+ // user accepted attempt to login
+ result = await showDialog({
+ title: `Git ${operation}`,
+ body: new GitPullPushDialog(model, operation, response.value),
+ buttons: [Dialog.okButton({ label: 'DISMISS' })]
+ });
+ } else {
+ break;
+ }
}
+}
- render() {
- return (
-
-
- {(_, change) => (
-
- {PathExt.basename(change.newValue || '')}
-
- )}
-
+/**
+ * Select a Git repository folder
+ *
+ * @param model Git extension model
+ * @param fileModel file browser model
+ */
+async function selectGitRepository(
+ model: IGitExtension,
+ fileModel: FileBrowserModel
+) {
+ const result = await FileDialog.getExistingDirectory({
+ iconRegistry: fileModel.iconRegistry,
+ manager: fileModel.manager,
+ title: 'Select a Git repository folder'
+ });
+
+ if (result.button.accept) {
+ const folder = result.value[0];
+ if (model.repositoryPinned) {
+ model.pathRepository = folder.path;
+ } else if (fileModel.path !== folder.path) {
+ // Change current filebrowser path
+ // => will be propagated to path repository
+ fileModel.cd(`/${folder.path}`);
+ }
+ }
+}
+
+/**
+ * React function component to render the toolbar and path header component
+ *
+ * @param props Properties for the path header component
+ */
+export const PathHeader: React.FunctionComponent
= (
+ props: IPathHeaderProps
+) => {
+ const [pin, setPin] = React.useState(props.model.repositoryPinned);
+
+ React.useEffect(() => {
+ props.model.restored.then(() => {
+ setPin(props.model.repositoryPinned);
+ });
+ });
+
+ return (
+
+
- );
- }
-
- /**
- * Displays the error dialog when the Git Push/Pull operation fails.
- * @param title the title of the error dialog
- * @param body the message to be shown in the body of the modal.
- */
- private async showGitPushPullDialog(
- model: IGitExtension,
- operation: Operation
- ): Promise {
- let result = await showDialog({
- title: `Git ${operation}`,
- body: new GitPullPushDialog(model, operation),
- buttons: [Dialog.okButton({ label: 'DISMISS' })]
- });
- let retry = false;
- while (!result.button.accept) {
- retry = true;
+
+ {(_, change) => {
+ const pathStyles = [repoPathStyle];
+ if (!change.newValue) {
+ pathStyles.push(noRepoPathStyle);
+ }
- let response = await showDialog({
- title: 'Git credentials required',
- body: new GitCredentialsForm(
- 'Enter credentials for remote repository',
- retry ? 'Incorrect username or password.' : ''
- ),
- buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })]
- });
+ let pinTitle = 'the repository path';
+ if (pin) {
+ pinTitle = 'Unpin ' + pinTitle;
+ } else {
+ pinTitle = 'Pin ' + pinTitle;
+ }
- if (response.button.accept) {
- // user accepted attempt to login
- result = await showDialog({
- title: `Git ${operation}`,
- body: new GitPullPushDialog(model, operation, response.value),
- buttons: [Dialog.okButton({ label: 'DISMISS' })]
- });
- } else {
- break;
- }
- }
- }
-}
+ return (
+
+
+ {
+ selectGitRepository(props.model, props.fileBrowserModel);
+ }}
+ >
+ {change.newValue
+ ? PathExt.basename(change.newValue)
+ : 'Click to select a Git repository.'}
+
+
+ );
+ }}
+
+
+
+ );
+};
diff --git a/src/index.ts b/src/index.ts
index a18ab0f14..2a54ad5ed 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,11 @@ import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
-import { IChangedArgs, ISettingRegistry } from '@jupyterlab/coreutils';
+import {
+ IChangedArgs,
+ ISettingRegistry,
+ IStateDB
+} from '@jupyterlab/coreutils';
import {
FileBrowser,
FileBrowserModel,
@@ -14,11 +18,11 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { defaultIconRegistry } from '@jupyterlab/ui-components';
import { Menu } from '@phosphor/widgets';
import { addCommands, CommandIDs } from './gitMenuCommands';
-import { GitExtension } from './model';
+import { GitExtension, PLUGIN_ID } from './model';
import { registerGitIcons } from './style/icons';
import { IGitExtension } from './tokens';
import { addCloneButton } from './widgets/gitClone';
-import { GitWidget } from './widgets/GitWidget';
+import { createGitWidget } from './widgets/GitWidget';
export { Git, IGitExtension } from './tokens';
@@ -37,13 +41,14 @@ const RESOURCES = [
* The default running sessions extension.
*/
const plugin: JupyterFrontEndPlugin = {
- id: '@jupyterlab/git:plugin',
+ id: PLUGIN_ID,
requires: [
IMainMenu,
ILayoutRestorer,
IFileBrowserFactory,
IRenderMimeRegistry,
- ISettingRegistry
+ ISettingRegistry,
+ IStateDB
],
provides: IGitExtension,
activate,
@@ -64,7 +69,8 @@ async function activate(
restorer: ILayoutRestorer,
factory: IFileBrowserFactory,
renderMime: IRenderMimeRegistry,
- settingRegistry: ISettingRegistry
+ settingRegistry: ISettingRegistry,
+ state: IStateDB
): Promise {
let settings: ISettingRegistry.ISettings;
@@ -81,17 +87,25 @@ async function activate(
console.error(`Failed to load settings for the Git Extension.\n${error}`);
}
// Create the Git model
- const gitExtension = new GitExtension(app, settings);
+ const gitExtension = new GitExtension(app, settings, state);
// Whenever we restore the application, sync the Git extension path
- Promise.all([app.restored, filebrowser.model.restored]).then(() => {
- gitExtension.pathRepository = filebrowser.model.path;
+ Promise.all([
+ app.restored,
+ gitExtension.restored,
+ filebrowser.model.restored
+ ]).then(() => {
+ if (!gitExtension.repositoryPinned) {
+ gitExtension.pathRepository = filebrowser.model.path;
+ }
});
// Whenever the file browser path changes, sync the Git extension path
filebrowser.model.pathChanged.connect(
(model: FileBrowserModel, change: IChangedArgs) => {
- gitExtension.pathRepository = change.newValue;
+ if (!gitExtension.repositoryPinned) {
+ gitExtension.pathRepository = change.newValue;
+ }
}
);
// Whenever a user adds/renames/saves/deletes/modifies a file within the lab environment, refresh the Git status
@@ -100,7 +114,12 @@ async function activate(
// Provided we were able to load application settings, create the extension widgets
if (settings) {
// Create the Git widget sidebar
- const gitPlugin = new GitWidget(gitExtension, settings, renderMime);
+ const gitPlugin = createGitWidget(
+ gitExtension,
+ settings,
+ renderMime,
+ filebrowser.model
+ );
gitPlugin.id = 'jp-git-sessions';
gitPlugin.title.iconClass = 'jp-SideBar-tabIcon jp-GitIcon';
gitPlugin.title.caption = 'Git';
diff --git a/src/model.ts b/src/model.ts
index e07725ea5..839e4a810 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -3,7 +3,8 @@ import {
IChangedArgs,
PathExt,
Poll,
- ISettingRegistry
+ ISettingRegistry,
+ IStateDB
} from '@jupyterlab/coreutils';
import { ServerConnection } from '@jupyterlab/services';
import { CommandRegistry } from '@phosphor/commands';
@@ -15,14 +16,68 @@ import { IGitExtension, Git } from './tokens';
// Default refresh interval (in milliseconds) for polling the current Git status (NOTE: this value should be the same value as in the plugin settings schema):
const DEFAULT_REFRESH_INTERVAL = 3000; // ms
+export const PLUGIN_ID = '@jupyterlab/git:plugin';
+
+/**
+ * State variables of @jupyterlab/git extension
+ */
+interface IGitState {
+ /**
+ * Is the repository path pinned? i.e. not connected to the file browser
+ */
+ isRepositoryPin: boolean;
+ /**
+ * Git path repository
+ */
+ pathRepository: string | null;
+}
+
/** Main extension class */
export class GitExtension implements IGitExtension {
constructor(
app: JupyterFrontEnd = null,
- settings?: ISettingRegistry.ISettings
+ settings: ISettingRegistry.ISettings = null,
+ state: IStateDB = null
) {
const model = this;
this._app = app;
+ this._stateDB = state;
+
+ this._state = {
+ isRepositoryPin: false,
+ pathRepository: null
+ };
+
+ // Load state extension
+ if (app) {
+ this._restored = app.restored.then(async () => {
+ if (state) {
+ try {
+ const value = await state.fetch(PLUGIN_ID);
+ if (value) {
+ const stateExtension: IGitState = value as any;
+ if (stateExtension.isRepositoryPin) {
+ const change: IChangedArgs = {
+ name: 'pathRepository',
+ newValue: stateExtension.pathRepository,
+ oldValue: this._state.pathRepository
+ };
+ this._state = stateExtension;
+ this._repositoryChanged.emit(change);
+ }
+ }
+ } catch (reason) {
+ console.error(
+ `Fail to fetch the state for ${PLUGIN_ID}.\n${reason}`
+ );
+ }
+ } else {
+ return Promise.resolve();
+ }
+ });
+ } else {
+ this._restored = Promise.resolve();
+ }
// Load the server root path
this._getServerRoot()
@@ -58,11 +113,9 @@ export class GitExtension implements IGitExtension {
* @param settings - settings registry
*/
function onSettingsChange(settings: ISettingRegistry.ISettings) {
- const freq = poll.frequency;
poll.frequency = {
- interval: settings.composite.refreshInterval as number,
- backoff: freq.backoff,
- max: freq.max
+ ...poll.frequency,
+ interval: settings.composite.refreshInterval as number
};
}
}
@@ -70,7 +123,7 @@ export class GitExtension implements IGitExtension {
/**
* The list of branch in the current repo
*/
- get branches() {
+ get branches(): Git.IBranch[] {
return this._branches;
}
@@ -81,7 +134,7 @@ export class GitExtension implements IGitExtension {
/**
* The current branch
*/
- get currentBranch() {
+ get currentBranch(): Git.IBranch | null {
return this._currentBranch;
}
@@ -127,22 +180,25 @@ export class GitExtension implements IGitExtension {
* null if not defined.
*/
get pathRepository(): string | null {
- return this._pathRepository;
+ return this._state.pathRepository;
}
set pathRepository(v: string | null) {
const change: IChangedArgs = {
name: 'pathRepository',
newValue: null,
- oldValue: this._pathRepository
+ oldValue: this._state.pathRepository
};
if (v === null) {
this._pendingReadyPromise += 1;
this._readyPromise.then(() => {
- this._pathRepository = null;
+ this._state.pathRepository = null;
this._pendingReadyPromise -= 1;
if (change.newValue !== change.oldValue) {
+ if (this._stateDB) {
+ this._stateDB.save(PLUGIN_ID, this._state as any);
+ }
this.refresh().then(() => this._repositoryChanged.emit(change));
}
});
@@ -153,13 +209,16 @@ export class GitExtension implements IGitExtension {
.then(r => {
const results = r[1];
if (results.code === 0) {
- this._pathRepository = results.top_repo_path;
+ this._state.pathRepository = results.top_repo_path;
change.newValue = results.top_repo_path;
} else {
- this._pathRepository = null;
+ this._state.pathRepository = null;
}
if (change.newValue !== change.oldValue) {
+ if (this._stateDB) {
+ this._stateDB.save(PLUGIN_ID, this._state as any);
+ }
this.refresh().then(() => this._repositoryChanged.emit(change));
}
})
@@ -180,6 +239,33 @@ export class GitExtension implements IGitExtension {
return this._repositoryChanged;
}
+ /**
+ * Is the Git repository path pinned?
+ */
+ get repositoryPinned(): boolean {
+ return this._state.isRepositoryPin;
+ }
+
+ set repositoryPinned(status: boolean) {
+ if (this._state.isRepositoryPin !== status) {
+ this._state.isRepositoryPin = status;
+ if (this._stateDB) {
+ this._stateDB
+ .save(PLUGIN_ID, this._state as any)
+ .catch(reason =>
+ console.error(`Fail to save the ${PLUGIN_ID} state.\n${reason}`)
+ );
+ }
+ }
+ }
+
+ /**
+ * Promise that resolves when state is first restored.
+ */
+ get restored(): Promise {
+ return this._restored;
+ }
+
get shell(): JupyterFrontEnd.IShell | null {
return this._app ? this._app.shell : null;
}
@@ -305,7 +391,9 @@ export class GitExtension implements IGitExtension {
* @param mark Mark to set
*/
addMark(fname: string, mark: boolean) {
- this._currentMarker.add(fname, mark);
+ if (this._currentMarker) {
+ this._currentMarker.add(fname, mark);
+ }
}
/**
@@ -315,7 +403,11 @@ export class GitExtension implements IGitExtension {
* @returns Mark of the file
*/
getMark(fname: string): boolean {
- return this._currentMarker.get(fname);
+ if (this._currentMarker) {
+ return this._currentMarker.get(fname);
+ } else {
+ return false;
+ }
}
/**
@@ -324,7 +416,9 @@ export class GitExtension implements IGitExtension {
* @param fname Filename
*/
toggleMark(fname: string) {
- this._currentMarker.toggle(fname);
+ if (this._currentMarker) {
+ this._currentMarker.toggle(fname);
+ }
}
/**
@@ -993,25 +1087,27 @@ export class GitExtension implements IGitExtension {
return this._currentMarker;
}
- private _status: Git.IStatusFileResult[] = [];
- private _pathRepository: string | null = null;
- private _branches: Git.IBranch[];
- private _currentBranch: Git.IBranch;
- private _serverRoot: string;
private _app: JupyterFrontEnd | null;
+ private _branches: Git.IBranch[] = [];
+ private _currentBranch: Git.IBranch | null = null;
+ private _currentMarker: BranchMarker = null;
private _diffProviders: { [key: string]: Git.IDiffCallback } = {};
+ private _headChanged = new Signal(this);
private _isDisposed = false;
private _markerCache: Markers = new Markers(() => this._markChanged.emit());
- private _currentMarker: BranchMarker = null;
- private _readyPromise: Promise = Promise.resolve();
+ private _markChanged = new Signal(this);
private _pendingReadyPromise = 0;
private _poll: Poll;
- private _headChanged = new Signal(this);
- private _markChanged = new Signal(this);
+ private _readyPromise: Promise = Promise.resolve();
private _repositoryChanged = new Signal<
IGitExtension,
IChangedArgs
>(this);
+ private _restored: Promise;
+ private _serverRoot: string;
+ private _state: IGitState;
+ private _stateDB: IStateDB | null = null;
+ private _status: Git.IStatusFileResult[] = [];
private _statusChanged = new Signal(
this
);
diff --git a/src/style/BranchHeaderStyle.ts b/src/style/BranchHeaderStyle.ts
index 752aa01e4..aad4851e3 100644
--- a/src/style/BranchHeaderStyle.ts
+++ b/src/style/BranchHeaderStyle.ts
@@ -8,7 +8,6 @@ export const branchStyle = style({
});
export const selectedHeaderStyle = style({
- borderTop: 'var(--jp-border-width) solid var(--jp-border-color2)',
paddingBottom: 'var(--jp-border-width)'
});
diff --git a/src/style/GitPanelStyle.ts b/src/style/GitPanelStyle.ts
index 1043ef8de..edc9608bb 100644
--- a/src/style/GitPanelStyle.ts
+++ b/src/style/GitPanelStyle.ts
@@ -4,16 +4,8 @@ export const panelContainerStyle = style({
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
- height: '100%'
-});
-
-export const panelWarningStyle = style({
- textAlign: 'center',
- marginTop: '9px'
-});
-
-export const findRepoButtonStyle = style({
- color: 'white',
- backgroundColor: 'var(--jp-brand-color1)',
- marginTop: '5px'
+ height: '100%',
+ color: 'var(--jp-ui-font-color1)',
+ background: 'var(--jp-layout-color1)',
+ fontSize: 'var(--jp-ui-font-size1)'
});
diff --git a/src/style/GitWidgetStyle.ts b/src/style/GitWidgetStyle.ts
deleted file mode 100644
index 115a5e042..000000000
--- a/src/style/GitWidgetStyle.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { style } from 'typestyle';
-
-export const gitWidgetStyle = style({
- display: 'flex',
- flexDirection: 'column',
- minWidth: '300px',
- color: 'var(--jp-ui-font-color1)',
- background: 'var(--jp-layout-color1)',
- fontSize: 'var(--jp-ui-font-size1)'
-});
diff --git a/src/style/PathHeaderStyle.ts b/src/style/PathHeaderStyle.ts
index 347548e3c..7c9e65b56 100644
--- a/src/style/PathHeaderStyle.ts
+++ b/src/style/PathHeaderStyle.ts
@@ -1,6 +1,8 @@
import { style } from 'typestyle';
-export const repoStyle = style({
+// Toolbar styles
+
+export const toolBarStyle = style({
display: 'flex',
flexDirection: 'row',
backgroundColor: 'var(--jp-layout-color1)',
@@ -8,17 +10,6 @@ export const repoStyle = style({
minHeight: '35px'
});
-export const repoPathStyle = style({
- fontSize: 'var(--jp-ui-font-size1)',
- marginRight: '4px',
- paddingLeft: '4px',
- textOverflow: 'ellipsis',
- overflow: 'hidden',
- whiteSpace: 'nowrap',
- verticalAlign: 'middle',
- lineHeight: '33px'
-});
-
export const repoRefreshStyle = style({
width: 'var(--jp-private-running-button-width)',
background: 'var(--jp-layout-color1)',
@@ -90,3 +81,74 @@ export const gitPullStyle = style({
}
}
});
+
+// Path styles
+
+export const repoStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ margin: '4px 12px'
+});
+
+export const pinIconStyle = style({
+ position: 'absolute',
+ cursor: 'pointer',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ margin: '4px'
+});
+
+export const repoPinStyle = style({
+ background: 'var(--jp-layout-color1)',
+ position: 'relative',
+ display: 'inline-block',
+ width: '24px',
+ height: '24px',
+ flex: '0 0 auto',
+
+ $nest: {
+ input: {
+ opacity: 0,
+ height: 0,
+ width: 0
+ },
+
+ 'input:checked + span': {
+ transform: 'rotate(-45deg)'
+ },
+
+ '&:hover': {
+ backgroundColor: 'var(--jp-layout-color2)'
+ }
+ }
+});
+
+export const repoPathStyle = style({
+ flex: '1 1 auto',
+ fontSize: 'var(--jp-ui-font-size1)',
+ padding: '0px 4px 0px 4px',
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ verticalAlign: 'middle',
+ lineHeight: '24px',
+
+ $nest: {
+ '&:hover': {
+ backgroundColor: 'var(--jp-layout-color2)'
+ }
+ }
+});
+
+export const noRepoPathStyle = style({
+ color: 'var(--jp-ui-font-color2)'
+});
+
+// Separator line style
+
+export const separatorStyle = style({
+ flex: '0 0 auto',
+ borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)'
+});
diff --git a/src/style/icons.ts b/src/style/icons.ts
index c750392d9..a5c5164b3 100644
--- a/src/style/icons.ts
+++ b/src/style/icons.ts
@@ -4,6 +4,7 @@ import { IIconRegistry } from '@jupyterlab/ui-components';
import gitSvg from '../../style/images/git-icon.svg';
import deletionsMadeSvg from '../../style/images/deletions-made-icon.svg';
import insertionsMadeSvg from '../../style/images/insertions-made-icon.svg';
+import pinSvg from '../../style/images/pin.svg';
export function registerGitIcons(iconRegistry: IIconRegistry) {
iconRegistry.addIcon(
@@ -18,6 +19,10 @@ export function registerGitIcons(iconRegistry: IIconRegistry) {
{
name: 'git-insertionsMade',
svg: insertionsMadeSvg
+ },
+ {
+ name: 'git-pin',
+ svg: pinSvg
}
);
}
diff --git a/src/tokens.ts b/src/tokens.ts
index a318b4a06..4f54a4023 100644
--- a/src/tokens.ts
+++ b/src/tokens.ts
@@ -18,13 +18,25 @@ export interface IGitExtension extends IDisposable {
/**
* The current branch
*/
- currentBranch: Git.IBranch;
+ currentBranch: Git.IBranch | null;
/**
* A signal emitted when the HEAD of the git repository changes.
*/
readonly headChanged: ISignal;
+ /**
+ * Test whether the model is ready;
+ * i.e. if the top folder repository has been found.
+ */
+ isReady: boolean;
+
+ /**
+ * A promise that fulfills when the model is ready;
+ * i.e. if the top folder repository has been found.
+ */
+ ready: Promise;
+
/**
* Top level path of the current git repository
*/
@@ -36,16 +48,14 @@ export interface IGitExtension extends IDisposable {
readonly repositoryChanged: ISignal>;
/**
- * Test whether the model is ready;
- * i.e. if the top folder repository has been found.
+ * Is the Git repository path pinned?
*/
- isReady: boolean;
+ repositoryPinned: boolean;
/**
- * A promise that fulfills when the model is ready;
- * i.e. if the top folder repository has been found.
+ * Promise that resolves when state is first restored.
*/
- ready: Promise;
+ readonly restored: Promise;
/**
* Files list resulting of a git status call.
@@ -114,7 +124,7 @@ export interface IGitExtension extends IDisposable {
/** Make request to switch current working branch,
* create new branch if needed,
* or discard a specific file change or all changes
- * TODO: Refactor into seperate endpoints for each kind of checkout request
+ * TODO: Refactor into separate endpoints for each kind of checkout request
*
* If a branch name is provided, check it out (with or without creating it)
* If a filename is provided, check the file out
diff --git a/src/widgets/GitWidget.tsx b/src/widgets/GitWidget.tsx
index de55a8ea8..52a7a3a42 100644
--- a/src/widgets/GitWidget.tsx
+++ b/src/widgets/GitWidget.tsx
@@ -1,42 +1,25 @@
import { ReactWidget } from '@jupyterlab/apputils';
+import { ISettingRegistry } from '@jupyterlab/coreutils';
+import { FileBrowserModel } from '@jupyterlab/filebrowser';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
-import { Widget } from '@phosphor/widgets';
import * as React from 'react';
import { GitPanel } from '../components/GitPanel';
import { GitExtension } from '../model';
-import { gitWidgetStyle } from '../style/GitWidgetStyle';
-import { ISettingRegistry } from '@jupyterlab/coreutils';
/**
- * A class that exposes the git plugin Widget.
+ * create the git plugin Widget.
*/
-export class GitWidget extends ReactWidget {
- constructor(
- model: GitExtension,
- settings: ISettingRegistry.ISettings,
- renderMime: IRenderMimeRegistry,
- options?: Widget.IOptions
- ) {
- super(options);
- this.node.id = 'GitSession-root';
- this.addClass(gitWidgetStyle);
-
- this._model = model;
- this._renderMime = renderMime;
- this._settings = settings;
- }
-
- render() {
- return (
-
- );
- }
-
- private _model: GitExtension;
- private _renderMime: IRenderMimeRegistry;
- private _settings: ISettingRegistry.ISettings;
-}
+export const createGitWidget = (
+ model: GitExtension,
+ settings: ISettingRegistry.ISettings,
+ renderMime: IRenderMimeRegistry,
+ fileBrowserModel: FileBrowserModel
+) =>
+ ReactWidget.create(
+
+ );
diff --git a/src/widgets/gitClone.tsx b/src/widgets/gitClone.tsx
index 415a3735f..bb7cfc61c 100644
--- a/src/widgets/gitClone.tsx
+++ b/src/widgets/gitClone.tsx
@@ -20,22 +20,18 @@ export function addCloneButton(model: IGitExtension, filebrowser: FileBrowser) {
'gitClone',
ReactWidget.create(
- {(_, change: IChangedArgs) => (
- {
- await doGitClone(model, filebrowser.model.path);
- filebrowser.model.refresh();
- }}
- tooltip={'Git Clone'}
+ {(_, change: IChangedArgs) => (
+
)}
@@ -151,3 +147,54 @@ class GitCloneForm extends Widget {
return encodeURIComponent(this.node.querySelector('input').value);
}
}
+
+/**
+ * Git clone toolbar button properties
+ */
+interface IGitCloneButtonProps {
+ /**
+ * Git extension model
+ */
+ model: IGitExtension;
+ /**
+ * File browser object
+ */
+ filebrowser: FileBrowser;
+ /**
+ * File browser path change
+ */
+ change: IChangedArgs;
+}
+
+const GitCloneButton: React.FunctionComponent = (
+ props: IGitCloneButtonProps
+) => {
+ const [enable, setEnable] = React.useState(false);
+
+ React.useEffect(() => {
+ model
+ .showTopLevel(change.newValue)
+ .then(answer => {
+ setEnable(answer.code !== 0);
+ })
+ .catch(reason => {
+ console.error(
+ `Fail to get the top level path for ${change.newValue}.\n${reason}`
+ );
+ });
+ });
+
+ const { model, filebrowser, change } = props;
+
+ return (
+ {
+ await doGitClone(model, filebrowser.model.path);
+ filebrowser.model.refresh();
+ }}
+ tooltip={'Git Clone'}
+ />
+ );
+};
diff --git a/style/images/pin.svg b/style/images/pin.svg
new file mode 100644
index 000000000..ace0ad687
--- /dev/null
+++ b/style/images/pin.svg
@@ -0,0 +1,9 @@
+
diff --git a/tests/GitExtension.spec.tsx b/tests/GitExtension.spec.tsx
index 6fd5ef01b..bce7bff2b 100644
--- a/tests/GitExtension.spec.tsx
+++ b/tests/GitExtension.spec.tsx
@@ -74,7 +74,8 @@ describe('IGitExtension', () => {
const app = {
commands: {
hasCommand: jest.fn().mockReturnValue(true)
- }
+ },
+ restored: Promise.resolve()
};
model = new GitExtension(app as any);
});
diff --git a/tests/test-components/GitPanel.spec.tsx b/tests/test-components/GitPanel.spec.tsx
index eb243f219..c36ad0fcf 100644
--- a/tests/test-components/GitPanel.spec.tsx
+++ b/tests/test-components/GitPanel.spec.tsx
@@ -55,6 +55,7 @@ function MockSettings() {
describe('GitPanel', () => {
describe('#commitStagedFiles()', () => {
const props: IGitSessionNodeProps = {
+ fileBrowserModel: null,
model: null,
renderMime: null,
settings: null
@@ -69,7 +70,8 @@ describe('GitPanel', () => {
const app = {
commands: {
hasCommand: jest.fn().mockReturnValue(true)
- }
+ },
+ restored: Promise.resolve()
};
props.model = new GitModel(app as any);
props.model.pathRepository = '/path/to/repo';
diff --git a/tests/test-components/PathHeader.spec.tsx b/tests/test-components/PathHeader.spec.tsx
index 550e94c41..93e41d53e 100644
--- a/tests/test-components/PathHeader.spec.tsx
+++ b/tests/test-components/PathHeader.spec.tsx
@@ -52,6 +52,7 @@ describe('PathHeader', function() {
props = {
model: model,
+ fileBrowserModel: null,
refresh: async () => {}
};
});