Skip to content

Commit 4a36555

Browse files
author
Jaipreet Singh
committed
Visual diff for Notebook files
This change introduces the feature to be able to diff ipynb from various points inside the Git experience. The currently supported points are the Changed, Staged areas, and the History panel. The <Diff /> component maintains a registry of DiffProvider components and delegates to each provider based on the file extension. Note * This change is off the pre 1.0 version of the Git extension because NBDime isn't upgraded to 1.0
1 parent 1f6a016 commit 4a36555

32 files changed

+1497
-86
lines changed

src/components/FileItem.tsx

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
33
import {
44
changeStageButtonStyle,
55
changeStageButtonLeftStyle,
6-
discardFileButtonStyle
6+
discardFileButtonStyle,
7+
diffFileButtonStyle
78
} from '../componentsStyle/GitStageStyle';
89

910
import {
@@ -17,7 +18,6 @@ import {
1718
selectedFileChangedLabelStyle,
1819
fileChangedLabelBrandStyle,
1920
fileChangedLabelInfoStyle,
20-
discardWarningStyle,
2121
fileButtonStyle,
2222
fileGitButtonStyle,
2323
discardFileButtonSelectedStyle,
@@ -29,6 +29,10 @@ import { classes } from 'typestyle';
2929
import * as React from 'react';
3030

3131
import { showDialog, Dialog } from '@jupyterlab/apputils';
32+
import { ISpecialRef } from './diff/model';
33+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
34+
import { isDiffSupported } from './diff/Diff';
35+
import { openDiffView } from './diff/DiffWidget';
3236

3337
export interface IFileItemProps {
3438
topRepoPath: string;
@@ -55,6 +59,7 @@ export interface IFileItemProps {
5559
disableFile: boolean;
5660
toggleDisableFiles: Function;
5761
sideBarExpanded: boolean;
62+
renderMime: IRenderMimeRegistry;
5863
}
5964

6065
export class FileItem extends React.Component<IFileItemProps, {}> {
@@ -181,6 +186,15 @@ export class FileItem extends React.Component<IFileItemProps, {}> {
181186
}
182187
}
183188

189+
getDiffFileIconClass() {
190+
return classes(
191+
fileButtonStyle,
192+
changeStageButtonStyle,
193+
fileGitButtonStyle,
194+
diffFileButtonStyle
195+
);
196+
}
197+
184198
getDiscardFileIconClass() {
185199
if (this.showDiscardWarning()) {
186200
return classes(
@@ -206,10 +220,6 @@ export class FileItem extends React.Component<IFileItemProps, {}> {
206220
}
207221
}
208222

209-
getDiscardWarningClass() {
210-
return discardWarningStyle;
211-
}
212-
213223
/**
214224
* Callback method discarding unstanged changes for selected file.
215225
* It shows modal asking for confirmation and when confirmed make
@@ -236,6 +246,7 @@ export class FileItem extends React.Component<IFileItemProps, {}> {
236246
this.props.updateSelectedDiscardFile(-1);
237247
});
238248
}
249+
239250
render() {
240251
return (
241252
<div
@@ -283,16 +294,49 @@ export class FileItem extends React.Component<IFileItemProps, {}> {
283294
{this.getFileChangedLabel(this.props.file.y)}
284295
</span>
285296
{this.props.stage === 'Changed' && (
286-
<button
287-
className={`jp-Git-button ${this.getDiscardFileIconClass()}`}
288-
title={'Discard this change'}
289-
onClick={() => {
290-
this.discardSelectedFileChanges();
291-
}}
292-
/>
297+
<React.Fragment>
298+
<button
299+
className={`jp-Git-button ${this.getDiscardFileIconClass()}`}
300+
title={'Discard this change'}
301+
onClick={() => {
302+
this.discardSelectedFileChanges();
303+
}}
304+
/>
305+
{isDiffSupported(this.props.file.to) &&
306+
this.diffButton({ specialRef: 'WORKING' })}
307+
</React.Fragment>
293308
)}
309+
{this.props.stage === 'Staged' &&
310+
isDiffSupported(this.props.file.to) &&
311+
this.diffButton({ specialRef: 'INDEX' })}
294312
</span>
295313
</div>
296314
);
297315
}
316+
317+
/**
318+
* Creates a button element which is used to request diff from within the
319+
* Git panel.
320+
*
321+
* @param currentRef the ref to diff against the git 'HEAD' ref
322+
*/
323+
private diffButton(currentRef: ISpecialRef): JSX.Element {
324+
return (
325+
<button
326+
className={`jp-Git-button ${this.getDiffFileIconClass()}`}
327+
title={'Diff this file'}
328+
onClick={() => {
329+
openDiffView(
330+
this.props.file.to,
331+
this.props.app,
332+
{
333+
previousRef: { gitRef: 'HEAD' },
334+
currentRef: { specialRef: currentRef.specialRef }
335+
},
336+
this.props.renderMime
337+
);
338+
}}
339+
/>
340+
);
341+
}
298342
}

src/components/FileList.tsx

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,49 @@
1+
<<<<<<< HEAD
12
import { Dialog, showDialog } from '@jupyterlab/apputils';
23

34
import { JupyterFrontEnd } from '@jupyterlab/application';
45

56
import { Menu } from '@phosphor/widgets';
67

78
import { PathExt } from '@jupyterlab/coreutils';
8-
9-
import { Git, IGitShowPrefixResult } from '../git';
10-
9+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
10+
import * as React from 'react';
11+
import { openDiffView } from './diff/DiffWidget';
1112
import {
12-
moveFileUpButtonStyle,
13-
moveFileDownButtonStyle,
14-
moveFileUpButtonSelectedStyle,
15-
moveFileDownButtonSelectedStyle,
13+
folderFileIconSelectedStyle,
1614
folderFileIconStyle,
15+
genericFileIconSelectedStyle,
1716
genericFileIconStyle,
18-
yamlFileIconStyle,
19-
markdownFileIconStyle,
17+
imageFileIconSelectedStyle,
2018
imageFileIconStyle,
21-
spreadsheetFileIconStyle,
19+
jsonFileIconSelectedStyle,
2220
jsonFileIconStyle,
23-
pythonFileIconStyle,
21+
kernelFileIconSelectedStyle,
2422
kernelFileIconStyle,
25-
folderFileIconSelectedStyle,
26-
genericFileIconSelectedStyle,
27-
yamlFileIconSelectedStyle,
2823
markdownFileIconSelectedStyle,
29-
imageFileIconSelectedStyle,
30-
spreadsheetFileIconSelectedStyle,
31-
jsonFileIconSelectedStyle,
24+
markdownFileIconStyle,
25+
moveFileDownButtonSelectedStyle,
26+
moveFileDownButtonStyle,
27+
moveFileUpButtonSelectedStyle,
28+
moveFileUpButtonStyle,
3229
pythonFileIconSelectedStyle,
33-
kernelFileIconSelectedStyle
30+
pythonFileIconStyle,
31+
spreadsheetFileIconSelectedStyle,
32+
spreadsheetFileIconStyle,
33+
yamlFileIconSelectedStyle,
34+
yamlFileIconStyle
3435
} from '../componentsStyle/FileListStyle';
35-
36+
import { Git, IGitShowPrefixResult } from '../git';
3637
import { GitStage } from './GitStage';
3738

38-
import * as React from 'react';
39-
4039
export namespace CommandIDs {
4140
export const gitFileOpen = 'gf:Open';
4241
export const gitFileUnstage = 'gf:Unstage';
4342
export const gitFileStage = 'gf:Stage';
4443
export const gitFileTrack = 'gf:Track';
45-
export const gitFileUntrack = 'gf:Untrack';
4644
export const gitFileDiscard = 'gf:Discard';
45+
export const gitFileDiffWorking = 'gf:DiffWorking';
46+
export const gitFileDiffIndex = 'gf:DiffIndex';
4747
}
4848

4949
export interface IFileListState {
@@ -79,6 +79,7 @@ export interface IFileListProps {
7979
refresh: any;
8080
sideBarExpanded: boolean;
8181
display: boolean;
82+
renderMime: IRenderMimeRegistry;
8283
}
8384

8485
export class FileList extends React.Component<IFileListProps, IFileListState> {
@@ -130,6 +131,43 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
130131
}
131132
});
132133
}
134+
135+
if (!commands.hasCommand(CommandIDs.gitFileDiffWorking)) {
136+
commands.addCommand(CommandIDs.gitFileDiffWorking, {
137+
label: 'Diff',
138+
caption: 'Diff selected file',
139+
execute: () => {
140+
openDiffView(
141+
this.state.contextMenuFile,
142+
this.props.app,
143+
{
144+
currentRef: { specialRef: 'WORKING' },
145+
previousRef: { gitRef: 'HEAD' }
146+
},
147+
this.props.renderMime
148+
);
149+
}
150+
});
151+
}
152+
153+
if (!commands.hasCommand(CommandIDs.gitFileDiffIndex)) {
154+
commands.addCommand(CommandIDs.gitFileDiffIndex, {
155+
label: 'Diff',
156+
caption: 'Diff selected file',
157+
execute: () => {
158+
openDiffView(
159+
this.state.contextMenuFile,
160+
this.props.app,
161+
{
162+
currentRef: { specialRef: 'INDEX' },
163+
previousRef: { gitRef: 'HEAD' }
164+
},
165+
this.props.renderMime
166+
);
167+
}
168+
});
169+
}
170+
133171
if (!commands.hasCommand(CommandIDs.gitFileStage)) {
134172
commands.addCommand(CommandIDs.gitFileStage, {
135173
label: 'Stage',
@@ -145,6 +183,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
145183
}
146184
});
147185
}
186+
148187
if (!commands.hasCommand(CommandIDs.gitFileTrack)) {
149188
commands.addCommand(CommandIDs.gitFileTrack, {
150189
label: 'Track',
@@ -197,6 +236,9 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
197236
this.state.contextMenuStaged.addItem({
198237
command: CommandIDs.gitFileUnstage
199238
});
239+
this.state.contextMenuStaged.addItem({
240+
command: CommandIDs.gitFileDiffIndex
241+
});
200242

201243
this.state.contextMenuUnstaged.addItem({ command: CommandIDs.gitFileOpen });
202244
this.state.contextMenuUnstaged.addItem({
@@ -205,6 +247,9 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
205247
this.state.contextMenuUnstaged.addItem({
206248
command: CommandIDs.gitFileDiscard
207249
});
250+
this.state.contextMenuUnstaged.addItem({
251+
command: CommandIDs.gitFileDiffWorking
252+
});
208253

209254
this.state.contextMenuUntracked.addItem({
210255
command: CommandIDs.gitFileOpen
@@ -301,7 +346,6 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
301346
updateSelectedStage = (stage: string): void => {
302347
this.setState({ selectedStage: stage });
303348
};
304-
305349
/** Open a file in the git listing */
306350
async openListedFile(
307351
typeX: string,
@@ -497,6 +541,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
497541
disableOthers={null}
498542
isDisabled={this.state.disableStaged}
499543
sideBarExpanded={this.props.sideBarExpanded}
544+
renderMime={this.props.renderMime}
500545
/>
501546
<GitStage
502547
heading={'Changed'}
@@ -530,6 +575,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
530575
disableOthers={this.disableStagesForDiscardAll}
531576
isDisabled={this.state.disableUnstaged}
532577
sideBarExpanded={this.props.sideBarExpanded}
578+
renderMime={this.props.renderMime}
533579
/>
534580
<GitStage
535581
heading={'Untracked'}
@@ -563,6 +609,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
563609
disableOthers={null}
564610
isDisabled={this.state.disableUntracked}
565611
sideBarExpanded={this.props.sideBarExpanded}
612+
renderMime={this.props.renderMime}
566613
/>
567614
</div>
568615
)}

src/components/GitPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
panelWarningStyle,
2828
findRepoButtonStyle
2929
} from '../componentsStyle/GitPanelStyle';
30+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3031

3132
/** Interface for GitPanel component state */
3233
export interface IGitSessionNodeState {
@@ -54,6 +55,7 @@ export interface IGitSessionNodeState {
5455
export interface IGitSessionNodeProps {
5556
app: JupyterFrontEnd;
5657
diff: IDiffCallback;
58+
renderMime: IRenderMimeRegistry;
5759
}
5860

5961
/** A React component for the git extension's main display */
@@ -238,6 +240,7 @@ export class GitPanel extends React.Component<
238240
app={this.props.app}
239241
refresh={this.refresh}
240242
diff={this.props.diff}
243+
renderMime={this.props.renderMime}
241244
/>
242245
<PastCommits
243246
currentFileBrowserPath={this.state.currentFileBrowserPath}
@@ -251,6 +254,7 @@ export class GitPanel extends React.Component<
251254
refresh={this.refresh}
252255
diff={this.props.diff}
253256
sideBarExpanded={this.state.sideBarExpanded}
257+
renderMime={this.props.renderMime}
254258
/>
255259
</div>
256260
);

src/components/GitStage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { classes } from 'typestyle';
2121
import * as React from 'react';
2222

2323
import { showDialog, Dialog } from '@jupyterlab/apputils';
24+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2425

2526
export interface IGitStageProps {
2627
heading: string;
@@ -54,6 +55,7 @@ export interface IGitStageProps {
5455
isDisabled: boolean;
5556
disableOthers: Function;
5657
sideBarExpanded: boolean;
58+
renderMime: IRenderMimeRegistry;
5759
}
5860

5961
export interface IGitStageState {
@@ -187,6 +189,7 @@ export class GitStage extends React.Component<IGitStageProps, IGitStageState> {
187189
disableFile={this.props.disableFiles}
188190
toggleDisableFiles={this.props.toggleDisableFiles}
189191
sideBarExpanded={this.props.sideBarExpanded}
192+
renderMime={this.props.renderMime}
190193
/>
191194
);
192195
}

src/components/GitWidget.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { gitWidgetStyle } from '../componentsStyle/GitWidgetStyle';
1818

1919
import { IDiffCallback } from '../git';
2020

21+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2122
/**
2223
* An options object for creating a running sessions widget.
2324
*/
@@ -69,13 +70,16 @@ export class GitWidget extends Widget {
6970
constructor(
7071
app: JupyterFrontEnd,
7172
options: IOptions,
72-
diffFunction: IDiffCallback
73+
diffFunction: IDiffCallback,
74+
renderMime: IRenderMimeRegistry
7375
) {
7476
super({
7577
node: (options.renderer || defaultRenderer).createNode()
7678
});
7779
this.addClass(gitWidgetStyle);
78-
const element = <GitPanel app={app} diff={diffFunction} />;
80+
const element = (
81+
<GitPanel app={app} diff={diffFunction} renderMime={renderMime} />
82+
);
7983
this.component = ReactDOM.render(element, this.node);
8084
this.component.refresh();
8185
}

0 commit comments

Comments
 (0)