Skip to content

Commit cccc3fe

Browse files
authored
Merge pull request #395 from jupyterlab/pathfix2
Use the path relative to server root for diffs
2 parents fbe5664 + a751b48 commit cccc3fe

File tree

10 files changed

+175
-66
lines changed

10 files changed

+175
-66
lines changed

jupyterlab_git/handlers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,14 @@ def post(self):
443443
self.finish(json.dumps(response))
444444

445445

446+
class GitServerRootHandler(GitHandler):
447+
448+
def get(self):
449+
# Similar to https://github.com/jupyter/nbdime/blob/master/nbdime/webapp/nb_server_extension.py#L90-L91
450+
self.finish(json.dumps({
451+
"server_root": getattr(self.contents_manager, 'root_dir', None)
452+
}))
453+
446454
def setup_handlers(web_app):
447455
"""
448456
Setups all of the git command handlers.
@@ -471,7 +479,8 @@ def setup_handlers(web_app):
471479
("/git/clone", GitCloneHandler),
472480
("/git/upstream", GitUpstreamHandler),
473481
("/git/config", GitConfigHandler),
474-
("/git/changed_files", GitChangedFilesHandler)
482+
("/git/changed_files", GitChangedFilesHandler),
483+
("/git/server_root", GitServerRootHandler)
475484
]
476485

477486
# add the baseurl to our paths

src/components/FileItem.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
import { JupyterFrontEnd } from '@jupyterlab/application';
22

33
import {
4-
changeStageButtonStyle,
54
changeStageButtonLeftStyle,
6-
discardFileButtonStyle,
7-
diffFileButtonStyle
5+
changeStageButtonStyle,
6+
diffFileButtonStyle,
7+
discardFileButtonStyle
88
} from '../componentsStyle/GitStageStyle';
99

1010
import {
11-
fileStyle,
12-
selectedFileStyle,
13-
expandedFileStyle,
1411
disabledFileStyle,
15-
fileIconStyle,
16-
fileLabelStyle,
17-
fileChangedLabelStyle,
18-
selectedFileChangedLabelStyle,
12+
discardFileButtonSelectedStyle,
13+
expandedFileStyle,
14+
fileButtonStyle,
1915
fileChangedLabelBrandStyle,
2016
fileChangedLabelInfoStyle,
21-
fileButtonStyle,
17+
fileChangedLabelStyle,
2218
fileGitButtonStyle,
23-
discardFileButtonSelectedStyle,
19+
fileIconStyle,
20+
fileLabelStyle,
21+
fileStyle,
22+
selectedFileChangedLabelStyle,
23+
selectedFileStyle,
2424
sideBarExpandedFileLabelStyle
2525
} from '../componentsStyle/FileItemStyle';
2626

2727
import { classes } from 'typestyle';
2828

2929
import * as React from 'react';
3030

31-
import { showDialog, Dialog } from '@jupyterlab/apputils';
31+
import { Dialog, showDialog } from '@jupyterlab/apputils';
3232
import { ISpecialRef } from './diff/model';
3333
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3434
import { isDiffSupported } from './diff/Diff';
@@ -325,15 +325,16 @@ export class FileItem extends React.Component<IFileItemProps, {}> {
325325
<button
326326
className={`jp-Git-button ${this.getDiffFileIconClass()}`}
327327
title={'Diff this file'}
328-
onClick={() => {
329-
openDiffView(
328+
onClick={async () => {
329+
await openDiffView(
330330
this.props.file.to,
331331
this.props.app,
332332
{
333333
previousRef: { gitRef: 'HEAD' },
334334
currentRef: { specialRef: currentRef.specialRef }
335335
},
336-
this.props.renderMime
336+
this.props.renderMime,
337+
this.props.topRepoPath
337338
);
338339
}}
339340
/>

src/components/FileList.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,16 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
129129
commands.addCommand(CommandIDs.gitFileDiffWorking, {
130130
label: 'Diff',
131131
caption: 'Diff selected file',
132-
execute: () => {
133-
openDiffView(
132+
execute: async () => {
133+
await openDiffView(
134134
this.state.contextMenuFile,
135135
this.props.app,
136136
{
137137
currentRef: { specialRef: 'WORKING' },
138138
previousRef: { gitRef: 'HEAD' }
139139
},
140-
this.props.renderMime
140+
this.props.renderMime,
141+
this.props.topRepoPath
141142
);
142143
}
143144
});
@@ -147,15 +148,16 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
147148
commands.addCommand(CommandIDs.gitFileDiffIndex, {
148149
label: 'Diff',
149150
caption: 'Diff selected file',
150-
execute: () => {
151-
openDiffView(
151+
execute: async () => {
152+
await openDiffView(
152153
this.state.contextMenuFile,
153154
this.props.app,
154155
{
155156
currentRef: { specialRef: 'INDEX' },
156157
previousRef: { gitRef: 'HEAD' }
157158
},
158-
this.props.renderMime
159+
this.props.renderMime,
160+
this.props.topRepoPath
159161
);
160162
}
161163
});

src/components/SinglePastCommitInfo.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,16 @@ export class SinglePastCommitInfo extends React.Component<
217217
<button
218218
className={`${diffIconStyle}`}
219219
title={'View file changes'}
220-
onClick={() => {
221-
openDiffView(
220+
onClick={async () => {
221+
await openDiffView(
222222
modifiedFile.modified_file_path,
223223
this.props.app,
224224
{
225225
previousRef: { gitRef: this.props.data.pre_commit },
226226
currentRef: { gitRef: this.props.data.commit }
227227
},
228-
this.props.renderMime
228+
this.props.renderMime,
229+
this.props.topRepoPath
229230
);
230231
}}
231232
/>

src/components/diff/Diff.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export interface IDiffProps {
3434
*/
3535
export function Diff(props: IDiffProps) {
3636
const fileExtension = PathExt.extname(props.path).toLocaleLowerCase();
37-
3837
if (fileExtension in DIFF_PROVIDER_REGISTRY) {
3938
const DiffProvider = DIFF_PROVIDER_REGISTRY[fileExtension];
4039
return <DiffProvider {...props} />;

src/components/diff/DiffWidget.tsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,27 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
77
import { ReactWidget, showDialog } from '@jupyterlab/apputils';
88
import { PathExt } from '@jupyterlab/coreutils';
99
import { style } from 'typestyle';
10+
import { httpGitRequest } from '../../git';
1011

1112
/**
1213
* Method to open a main menu panel to show the diff of a given Notebook file.
1314
* If one already exists, just activates the existing one.
1415
*
15-
* @param path the path relative to the Jupyter server root.
16+
* @param filePath the path relative to the Git repo root.
1617
* @param app The JupyterLab application instance
1718
* @param diffContext the context in which the diff is being requested
1819
* @param renderMime the renderMime registry instance from the
20+
* @param topRepoPath the Git repo root path.
1921
*/
20-
export function openDiffView(
21-
path: string,
22+
export async function openDiffView(
23+
filePath: string,
2224
app: JupyterFrontEnd,
2325
diffContext: IDiffContext,
24-
renderMime: IRenderMimeRegistry
26+
renderMime: IRenderMimeRegistry,
27+
topRepoPath: string
2528
) {
26-
if (isDiffSupported(path)) {
27-
const id = `nbdiff-${path}-${getRefValue(diffContext.currentRef)}`;
29+
if (isDiffSupported(filePath)) {
30+
const id = `nbdiff-${filePath}-${getRefValue(diffContext.currentRef)}`;
2831
let mainAreaItems = app.shell.widgets('main');
2932
let mainAreaItem = mainAreaItems.next();
3033
while (mainAreaItem) {
@@ -35,13 +38,17 @@ export function openDiffView(
3538
mainAreaItem = mainAreaItems.next();
3639
}
3740
if (!mainAreaItem) {
41+
const relativeFilePath: string = await getRelativeFilePath(
42+
filePath,
43+
topRepoPath
44+
);
3845
const nbDiffWidget = ReactWidget.create(
3946
<RenderMimeProvider value={renderMime}>
40-
<Diff path={path} diffContext={diffContext} />
47+
<Diff path={relativeFilePath} diffContext={diffContext} />
4148
</RenderMimeProvider>
4249
);
4350
nbDiffWidget.id = id;
44-
nbDiffWidget.title.label = PathExt.basename(path);
51+
nbDiffWidget.title.label = PathExt.basename(filePath);
4552
nbDiffWidget.title.iconClass = style({
4653
backgroundImage: 'var(--jp-icon-diff)'
4754
});
@@ -55,8 +62,40 @@ export function openDiffView(
5562
showDialog({
5663
title: 'Diff Not Supported',
5764
body: `Diff is not supported for ${PathExt.extname(
58-
path
65+
filePath
5966
).toLocaleLowerCase()} files.`
6067
});
6168
}
6269
}
70+
71+
let serverRootResultCache = null;
72+
/**
73+
* Gets the path of the file relative to the Jupyter server root. This resolves the server root path after calling
74+
* the `/git/server_root` REST API, and uses the Git repo root and the file path relative to the repo root to resolve
75+
* the rest.
76+
* @param path the file path relative to Git repo root
77+
* @param topRepoPath the Git repo root path
78+
*/
79+
export async function getRelativeFilePath(
80+
path: string,
81+
topRepoPath: string
82+
): Promise<string> {
83+
if (serverRootResultCache !== null) {
84+
return serverRootResultCache;
85+
} else {
86+
const response = await httpGitRequest('/git/server_root', 'GET', null);
87+
const responseData = await response.json();
88+
serverRootResultCache = PathExt.join(
89+
PathExt.relative(responseData['server_root'], topRepoPath),
90+
path
91+
);
92+
return serverRootResultCache;
93+
}
94+
}
95+
96+
/**
97+
* Visible for testing.
98+
*/
99+
export function clearCache() {
100+
serverRootResultCache = null;
101+
}

src/git.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,19 @@ export interface IIdentity {
132132
export function httpGitRequest(
133133
url: string,
134134
method: string,
135-
request: Object
135+
request: Object | null
136136
): Promise<Response> {
137-
let fullRequest = {
138-
method: method,
139-
body: JSON.stringify(request)
140-
};
137+
let fullRequest;
138+
if (request === null) {
139+
fullRequest = {
140+
method: method
141+
};
142+
} else {
143+
fullRequest = {
144+
method: method,
145+
body: JSON.stringify(request)
146+
};
147+
}
141148

142149
let setting = ServerConnection.makeSettings();
143150
let fullUrl = URLExt.join(setting.baseUrl, url);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'jest';
2+
import { httpGitRequest } from '../../src/git';
3+
import { getRelativeFilePath } from '../../src/components/diff/DiffWidget';
4+
import { clearCache } from '../../src/components/diff/DiffWidget';
5+
import { createTestResponse } from './testutils';
6+
7+
jest.mock('../../src/git');
8+
9+
describe('DiffWidget', () => {
10+
it('should return relative path correctly ', async function() {
11+
const testData = [
12+
[
13+
'somefolder/file',
14+
'/path/to/server/dir1/dir2/repo',
15+
'dir1/dir2/repo/somefolder/file'
16+
],
17+
['file', '/path/to/server/repo', 'repo/file'],
18+
['somefolder/file', '/path/to/server/repo', 'repo/somefolder/file'],
19+
['somefolder/file', '/path/to/server', 'somefolder/file'],
20+
['file', '/path/to/server', 'file']
21+
];
22+
23+
for (const testDatum of testData) {
24+
await testGetRelativeFilePath(testDatum[0], testDatum[1], testDatum[2]);
25+
}
26+
});
27+
28+
async function testGetRelativeFilePath(
29+
filePath: string,
30+
repoPath: string,
31+
expectedResult: string
32+
): Promise<void> {
33+
// Given
34+
const jsonResult: Promise<any> = Promise.resolve({
35+
server_root: '/path/to/server'
36+
});
37+
38+
(httpGitRequest as jest.Mock).mockReturnValue(
39+
createTestResponse(200, jsonResult)
40+
);
41+
42+
// When
43+
clearCache();
44+
const result = await getRelativeFilePath(filePath, repoPath);
45+
46+
// Then
47+
expect(result).toBe(expectedResult);
48+
expect(httpGitRequest).toHaveBeenCalled();
49+
expect(httpGitRequest).toBeCalledWith('/git/server_root', 'GET', null);
50+
}
51+
});

tests/test-components/NBDiff.spec.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CellDiff, NBDiff } from '../../src/components/diff/NbDiff';
66
import * as diffResponse from '../test-components/data/diffResponse.json';
77
import { httpGitRequest } from '../../src/git';
88
import { NBDiffHeader } from '../../src/components/diff/NBDiffHeader';
9+
import { createTestResponse } from './testutils';
910

1011
jest.mock('../../src/git');
1112

@@ -84,28 +85,3 @@ describe('NBDiff', () => {
8485
});
8586
});
8687
});
87-
88-
function createTestResponse(
89-
status: number,
90-
json: Promise<any>
91-
): Promise<Response> {
92-
const testResponse: Response = {
93-
status: status,
94-
json: () => json,
95-
headers: Headers as any,
96-
ok: true,
97-
redirected: false,
98-
statusText: 'foo',
99-
trailer: Promise.resolve(Headers as any),
100-
type: 'basic',
101-
url: '/foo/bar',
102-
clone: jest.fn<Response, []>(),
103-
body: null,
104-
bodyUsed: false,
105-
arrayBuffer: jest.fn<Promise<ArrayBuffer>, []>(),
106-
blob: jest.fn<Promise<Blob>, []>(),
107-
formData: jest.fn<Promise<FormData>, []>(),
108-
text: jest.fn<Promise<string>, []>()
109-
};
110-
return Promise.resolve(testResponse);
111-
}

tests/test-components/testutils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export function createTestResponse(
2+
status: number,
3+
json: Promise<any>
4+
): Promise<Response> {
5+
const testResponse: Response = {
6+
status: status,
7+
json: () => json,
8+
headers: Headers as any,
9+
ok: true,
10+
redirected: false,
11+
statusText: 'foo',
12+
trailer: Promise.resolve(Headers as any),
13+
type: 'basic',
14+
url: '/foo/bar',
15+
clone: jest.fn<Response, []>(),
16+
body: null,
17+
bodyUsed: false,
18+
arrayBuffer: jest.fn<Promise<ArrayBuffer>, []>(),
19+
blob: jest.fn<Promise<Blob>, []>(),
20+
formData: jest.fn<Promise<FormData>, []>(),
21+
text: jest.fn<Promise<string>, []>()
22+
};
23+
return Promise.resolve(testResponse);
24+
}

0 commit comments

Comments
 (0)