Skip to content

Commit 0d28c15

Browse files
authored
Merge pull request #711 from fcollonval/auto-backport-of-pr-658-on-0.11.x
Backport PR #658: Check git version and compatibility front/backend v…
2 parents b01ec12 + 3ccb89b commit 0d28c15

19 files changed

+607
-98
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jupyter labextension list
5757
- Be sure to be in a Git repository in the filebrowser tab
5858

5959
- Check the server log. If you see a warning with a 404 code similar to:
60-
`[W 00:27:41.800 LabApp] 404 GET /git/server_root?1576081660665`
60+
`[W 00:27:41.800 LabApp] 404 GET /git/settings?version=0.20.0`
6161

6262
Explicitly enable the server extension by running:
6363

jupyterlab_git/git.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
MAX_WAIT_FOR_LOCK_S = 5
2525
# How often should we check for the lock above to be free? This comes up more on things like NFS
2626
CHECK_LOCK_INTERVAL_S = 0.1
27+
# Parse Git version output
28+
GIT_VERSION_REGEX = re.compile(r"^git\sversion\s(?P<version>\d+(.\d+)*)")
2729

2830
execution_lock = tornado.locks.Lock()
2931

@@ -1109,3 +1111,17 @@ def remote_add(self, top_repo_path, url, name=DEFAULT_REMOTE_NAME):
11091111
"command": " ".join(cmd),
11101112
"message": my_error.decode("utf-8").strip()
11111113
}
1114+
1115+
async def version(self):
1116+
"""Return the Git command version.
1117+
1118+
If an error occurs, return None.
1119+
"""
1120+
command = ["git", "--version"]
1121+
code, output, _ = await execute(command, cwd=os.curdir)
1122+
if code == 0:
1123+
version = GIT_VERSION_REGEX.match(output)
1124+
if version is not None:
1125+
return version.group('version')
1126+
1127+
return None

jupyterlab_git/handlers.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from notebook.base.handlers import APIHandler
1010
from notebook.utils import url_path_join as ujoin, url2path
11+
from packaging.version import parse
1112

13+
from ._version import __version__
1214
from .git import DEFAULT_REMOTE_NAME
1315

1416
class GitHandler(APIHandler):
@@ -532,13 +534,31 @@ async def post(self):
532534
self.finish(json.dumps(response))
533535

534536

535-
class GitServerRootHandler(GitHandler):
537+
class GitSettingsHandler(GitHandler):
536538
@web.authenticated
537539
async def get(self):
540+
jlab_version = self.get_query_argument("version", None)
541+
if jlab_version is not None:
542+
jlab_version = str(parse(jlab_version))
543+
git_version = None
544+
try:
545+
git_version = await self.git.version()
546+
except Exception as error:
547+
self.log.debug("[jupyterlab_git] Failed to execute 'git' command: {!s}".format(error))
548+
server_version = str(parse(__version__))
538549
# Similar to https://github.com/jupyter/nbdime/blob/master/nbdime/webapp/nb_server_extension.py#L90-L91
539550
root_dir = getattr(self.contents_manager, "root_dir", None)
540551
server_root = None if root_dir is None else Path(root_dir).as_posix()
541-
self.finish(json.dumps({"server_root": server_root}))
552+
self.finish(
553+
json.dumps(
554+
{
555+
"frontendVersion": jlab_version,
556+
"gitVersion": git_version,
557+
"serverRoot": server_root,
558+
"serverVersion": server_version,
559+
}
560+
)
561+
)
542562

543563

544564
def setup_handlers(web_app):
@@ -569,7 +589,7 @@ def setup_handlers(web_app):
569589
("/git/remote/add", GitRemoteAddHandler),
570590
("/git/reset", GitResetHandler),
571591
("/git/reset_to_commit", GitResetToCommitHandler),
572-
("/git/server_root", GitServerRootHandler),
592+
("/git/settings", GitSettingsHandler),
573593
("/git/show_prefix", GitShowPrefixHandler),
574594
("/git/show_top_level", GitShowTopLevelHandler),
575595
("/git/status", GitStatusHandler),

jupyterlab_git/tests/test_settings.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import os
2+
from unittest.mock import Mock, patch
3+
4+
from packaging.version import parse
5+
6+
from jupyterlab_git import __version__
7+
from jupyterlab_git.handlers import GitSettingsHandler
8+
9+
from .testutils import ServerTest, assert_http_error, maybe_future
10+
11+
12+
class TestSettings(ServerTest):
13+
@patch("jupyterlab_git.git.execute")
14+
def test_git_get_settings_success(self, mock_execute):
15+
# Given
16+
git_version = "2.10.3"
17+
jlab_version = "2.1.42-alpha.24"
18+
mock_execute.return_value = maybe_future(
19+
(0, "git version {}.os_platform.42".format(git_version), "")
20+
)
21+
22+
# When
23+
response = self.tester.get(["settings"], params={"version": jlab_version})
24+
25+
# Then
26+
mock_execute.assert_called_once_with(["git", "--version"], cwd=os.curdir)
27+
28+
assert response.status_code == 200
29+
payload = response.json()
30+
assert payload == {
31+
"frontendVersion": str(parse(jlab_version)),
32+
"gitVersion": git_version,
33+
"serverRoot": self.notebook.notebook_dir,
34+
"serverVersion": str(parse(__version__)),
35+
}
36+
37+
@patch("jupyterlab_git.git.execute")
38+
def test_git_get_settings_no_git(self, mock_execute):
39+
# Given
40+
jlab_version = "2.1.42-alpha.24"
41+
mock_execute.side_effect = FileNotFoundError("[Errno 2] No such file or directory: 'git'")
42+
43+
# When
44+
response = self.tester.get(["settings"], params={"version": jlab_version})
45+
46+
# Then
47+
mock_execute.assert_called_once_with(["git", "--version"], cwd=os.curdir)
48+
49+
assert response.status_code == 200
50+
payload = response.json()
51+
assert payload == {
52+
"frontendVersion": str(parse(jlab_version)),
53+
"gitVersion": None,
54+
"serverRoot": self.notebook.notebook_dir,
55+
"serverVersion": str(parse(__version__)),
56+
}
57+
58+
@patch("jupyterlab_git.git.execute")
59+
def test_git_get_settings_no_jlab(self, mock_execute):
60+
# Given
61+
git_version = "2.10.3"
62+
mock_execute.return_value = maybe_future(
63+
(0, "git version {}.os_platform.42".format(git_version), "")
64+
)
65+
66+
# When
67+
response = self.tester.get(["settings"])
68+
69+
# Then
70+
mock_execute.assert_called_once_with(["git", "--version"], cwd=os.curdir)
71+
72+
assert response.status_code == 200
73+
payload = response.json()
74+
assert payload == {
75+
"frontendVersion": None,
76+
"gitVersion": git_version,
77+
"serverRoot": self.notebook.notebook_dir,
78+
"serverVersion": str(parse(__version__)),
79+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"jupyterlab-extension"
1414
],
1515
"scripts": {
16-
"build": "tsc",
16+
"build": "genversion --es6 --semi src/version.ts && tsc",
1717
"build:labextension": "jlpm && jlpm clean:labextension && mkdirp jupyterlab_git/labextension && cd jupyterlab_git/labextension && npm pack ../..",
1818
"clean": "rimraf lib tsconfig.tsbuildinfo",
1919
"clean:more": "jlpm clean && rimraf build dist MANIFEST",
@@ -92,6 +92,7 @@
9292
"eslint-config-prettier": "^6.10.1",
9393
"eslint-plugin-prettier": "^3.1.2",
9494
"eslint-plugin-react": "^7.19.0",
95+
"genversion": "^2.2.1",
9596
"husky": "1.3.1",
9697
"identity-obj-proxy": "^3.0.0",
9798
"jest": "^24",

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ def runPackLabextension():
6767
'Programming Language :: Python :: 3.5',
6868
'Programming Language :: Python :: 3.6',
6969
'Programming Language :: Python :: 3.7',
70+
'Programming Language :: Python :: 3.8',
7071
'Framework :: Jupyter',
7172
],
7273
install_requires = [
7374
'notebook',
7475
'nbdime >= 1.1.0, < 2.0.0',
76+
'packaging',
7577
'pexpect'
7678
],
7779
extras_require = {

src/index.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
JupyterFrontEndPlugin
55
} from '@jupyterlab/application';
66
import { IChangedArgs, ISettingRegistry } from '@jupyterlab/coreutils';
7+
import { Dialog, showErrorMessage } from '@jupyterlab/apputils';
78
import { FileBrowserModel, IFileBrowserFactory } from '@jupyterlab/filebrowser';
89
import { IMainMenu } from '@jupyterlab/mainmenu';
910
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
@@ -12,7 +13,8 @@ import { defaultIconRegistry } from '@jupyterlab/ui-components';
1213
import { addCommands, createGitMenu } from './commandsAndMenu';
1314
import { GitExtension } from './model';
1415
import { registerGitIcons } from './style/icons';
15-
import { IGitExtension } from './tokens';
16+
import { getServerSettings } from './server';
17+
import { Git, IGitExtension } from './tokens';
1618
import { addCloneButton } from './widgets/gitClone';
1719
import { GitWidget } from './widgets/GitWidget';
1820
import { addStatusBarWidget } from './widgets/StatusWidget';
@@ -54,11 +56,13 @@ async function activate(
5456
settingRegistry: ISettingRegistry,
5557
statusBar: IStatusBar
5658
): Promise<IGitExtension> {
59+
let gitExtension: GitExtension | null = null;
5760
let settings: ISettingRegistry.ISettings;
5861

5962
// Register Git icons with the icon registry
6063
registerGitIcons(defaultIconRegistry);
6164

65+
let serverSettings: Git.IServerSettings;
6266
// Get a reference to the default file browser extension
6367
const filebrowser = factory.defaultBrowser;
6468

@@ -68,8 +72,45 @@ async function activate(
6872
} catch (error) {
6973
console.error(`Failed to load settings for the Git Extension.\n${error}`);
7074
}
75+
try {
76+
serverSettings = await getServerSettings();
77+
const { frontendVersion, gitVersion, serverVersion } = serverSettings;
78+
79+
// Version validation
80+
if (!gitVersion) {
81+
throw new Error(
82+
'git command not found - please ensure you have Git > 2 installed'
83+
);
84+
} else {
85+
const gitVersion_ = gitVersion.split('.');
86+
if (Number.parseInt(gitVersion_[0]) < 2) {
87+
throw new Error(`git command version must be > 2; got ${gitVersion}.`);
88+
}
89+
}
90+
91+
if (frontendVersion && frontendVersion !== serverVersion) {
92+
throw new Error(
93+
'The versions of the JupyterLab Git server frontend and backend do not match. ' +
94+
`The @jupyterlab/git frontend extension has version: ${frontendVersion} ` +
95+
`while the python package has version ${serverVersion}. ` +
96+
'Please install identical version of jupyterlab-git Python package and the @jupyterlab/git extension. Try running: pip install --upgrade jupyterlab-git'
97+
);
98+
}
99+
} catch (error) {
100+
// If we fall here, nothing will be loaded in the frontend.
101+
console.error(
102+
'Failed to load the jupyterlab-git server extension settings',
103+
error
104+
);
105+
showErrorMessage(
106+
'Failed to load the jupyterlab-git server extension',
107+
error.message,
108+
[Dialog.warnButton({ label: 'DISMISS' })]
109+
);
110+
return null;
111+
}
71112
// Create the Git model
72-
const gitExtension = new GitExtension(app, settings);
113+
gitExtension = new GitExtension(serverSettings.serverRoot, app, settings);
73114

74115
// Whenever we restore the application, sync the Git extension path
75116
Promise.all([app.restored, filebrowser.model.restored]).then(() => {

src/model.ts

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { LinkedList } from '@phosphor/collections';
1313
import { httpGitRequest } from './git';
1414
import { IGitExtension, Git } from './tokens';
1515
import { decodeStage } from './utils';
16-
import { Dialog, showErrorMessage } from '@jupyterlab/apputils';
1716

1817
// 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):
1918
const DEFAULT_REFRESH_INTERVAL = 3000; // ms
@@ -30,27 +29,15 @@ export class GitExtension implements IGitExtension {
3029
* @returns extension model
3130
*/
3231
constructor(
32+
serverRoot: string,
3333
app: JupyterFrontEnd = null,
3434
settings?: ISettingRegistry.ISettings
3535
) {
3636
const self = this;
37+
this._serverRoot = serverRoot;
3738
this._app = app;
3839
this._settings = settings || null;
3940

40-
// Load the server root path
41-
this._getServerRoot()
42-
.then(root => {
43-
this._serverRoot = root;
44-
})
45-
.catch(reason => {
46-
console.error(`Fail to get the server root path.\n${reason}`);
47-
showErrorMessage(
48-
'Internal Error:',
49-
`Fail to get the server root path.\n\n${reason}`,
50-
[Dialog.warnButton({ label: 'DISMISS' })]
51-
);
52-
});
53-
5441
let interval: number;
5542
if (settings) {
5643
interval = settings.composite.refreshInterval as number;
@@ -1153,30 +1140,6 @@ export class GitExtension implements IGitExtension {
11531140
this._statusChanged.emit(this._status);
11541141
}
11551142

1156-
/**
1157-
* Retrieve the path of the root server directory.
1158-
*
1159-
* @returns promise which resolves upon retrieving the root server path
1160-
*/
1161-
private async _getServerRoot(): Promise<string> {
1162-
let response;
1163-
try {
1164-
response = await httpGitRequest('/git/server_root', 'GET', null);
1165-
} catch (err) {
1166-
throw new ServerConnection.NetworkError(err);
1167-
}
1168-
if (response.status === 404) {
1169-
throw new ServerConnection.ResponseError(
1170-
response,
1171-
'Git server extension is unavailable. Please ensure you have installed the ' +
1172-
'JupyterLab Git server extension by running: pip install --upgrade jupyterlab-git. ' +
1173-
'To confirm that the server extension is installed, run: jupyter serverextension list.'
1174-
);
1175-
}
1176-
const data = await response.json();
1177-
return data['server_root'];
1178-
}
1179-
11801143
/**
11811144
* Set the marker object for a repository path and branch.
11821145
*

src/server.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { URLExt } from '@jupyterlab/coreutils';
2+
import { ServerConnection } from '@jupyterlab/services';
3+
import { Git } from './tokens';
4+
import { httpGitRequest } from './git';
5+
import { version } from './version';
6+
7+
export async function getServerSettings(): Promise<Git.IServerSettings> {
8+
try {
9+
const endpoint = '/git/settings' + URLExt.objectToQueryString({ version });
10+
const response = await httpGitRequest(endpoint, 'GET', null);
11+
if (response.status === 404) {
12+
const message =
13+
'Git server extension is unavailable. Please ensure you have installed the ' +
14+
'JupyterLab Git server extension by running: pip install --upgrade jupyterlab-git. ' +
15+
'To confirm that the server extension is installed, run: jupyter serverextension list.';
16+
throw new ServerConnection.ResponseError(response, message);
17+
}
18+
let content: string | any = await response.text();
19+
if (content.length > 0) {
20+
content = JSON.parse(content);
21+
}
22+
if (!response.ok) {
23+
const message = content.message || content;
24+
console.error('Failed to get the server extension settings', message);
25+
throw new ServerConnection.ResponseError(response, message);
26+
}
27+
return content as Git.IServerSettings;
28+
} catch (error) {
29+
if (error instanceof ServerConnection.ResponseError) {
30+
throw error;
31+
} else {
32+
throw new Error(error);
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)