Skip to content

Commit e4b8012

Browse files
committed
Resolve notebook merge conflicts
1 parent 03526ed commit e4b8012

File tree

4 files changed

+80
-20
lines changed

4 files changed

+80
-20
lines changed

src/commandsAndMenu.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,24 @@ export function addCommands(
476476
const resolveButton = new ToolbarButton({
477477
label: trans.__('Mark as resolved'),
478478
onClick: async () => {
479-
try {
480-
const resolvedFile: string = await widget.getResolvedFile();
481-
await serviceManager.contents.save(model.filename, {
482-
type: 'file',
483-
format: 'text',
484-
content: resolvedFile
479+
if (!widget.isFileResolved) {
480+
const result = await showDialog({
481+
title: trans.__('Resolve with conflicts'),
482+
body: trans.__(
483+
'Are you sure you want to mark this file as resolved with merge conflicts?'
484+
)
485485
});
486+
487+
if (!result.button.accept) {
488+
return;
489+
}
490+
}
491+
492+
try {
493+
await serviceManager.contents.save(
494+
model.filename,
495+
await widget.getResolvedFile()
496+
);
486497
await gitModel.add(model.filename);
487498
await gitModel.refresh();
488499
} catch (reason) {

src/components/diff/NotebookDiff.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/* eslint-disable no-inner-declarations */
77

88
import { Toolbar } from '@jupyterlab/apputils';
9+
import { Contents } from '@jupyterlab/services';
910
import { INotebookContent } from '@jupyterlab/nbformat';
1011
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
1112
import { PromiseDelegate } from '@lumino/coreutils';
@@ -96,6 +97,11 @@ export const createNotebookDiff = async (
9697
'Hide unchanged cells';
9798
toolbar.addItem('hideUnchanged', new Widget({ node: label }));
9899

100+
if (model.hasConflict) {
101+
// FIXME: Merge view breaks when moving checkboxes to the toolbar
102+
// toolbar.addItem('clear-outputs', diffWidget.nbdWidget.widgets[0])
103+
}
104+
99105
// Connect toolbar checkbox and notebook diff widget
100106
diffWidget.areUnchangedCellsHidden = checkbox.checked;
101107
checkbox.onchange = () => {
@@ -159,20 +165,39 @@ export class NotebookDiff
159165
return this._model.hasConflict;
160166
}
161167

168+
get nbdWidget(): NotebookDiffWidget | NotebookMergeWidget {
169+
return this._nbdWidget;
170+
}
171+
162172
/**
163173
* Promise which fulfills when the widget is ready.
164174
*/
165175
get ready(): Promise<void> {
166176
return this._isReady;
167177
}
168178

179+
/**
180+
* Checks if the conflicted file has been resolved.
181+
*/
182+
get isFileResolved(): boolean {
183+
const widget = this.nbdWidget as NotebookMergeWidget;
184+
this._lastSerializeModel = widget.model.serialize();
185+
const validated = widget.validateMerged(this._lastSerializeModel);
186+
return JSON.stringify(this._lastSerializeModel) === JSON.stringify(validated)
187+
}
188+
169189
/**
170190
* Gets the file contents of a resolved merge conflict,
171191
* and rejects if unable to retrieve.
192+
*
193+
* @see https://github.com/jupyter/nbdime/blob/a74b538386d05e3e9c26753ad21faf9ff4d269d7/packages/webapp/src/app/save.ts#L20
172194
*/
173-
async getResolvedFile(): Promise<string> {
174-
// TODO: Implement
175-
return Promise.reject('TODO');
195+
async getResolvedFile(): Promise<Partial<Contents.IModel>> {
196+
return Promise.resolve({
197+
format: 'json',
198+
type: 'notebook',
199+
content: this._lastSerializeModel ?? (this.nbdWidget as NotebookMergeWidget).model.serialize()
200+
});
176201
}
177202

178203
/**
@@ -209,7 +234,7 @@ export class NotebookDiff
209234
? this.createMergeView.bind(this)
210235
: this.createDiffView.bind(this);
211236

212-
const nbdWidget = await createView(
237+
this._nbdWidget = await createView(
213238
challengerContent,
214239
referenceContent,
215240
baseContent
@@ -218,9 +243,9 @@ export class NotebookDiff
218243
while (this._scroller.widgets.length > 0) {
219244
this._scroller.widgets[0].dispose();
220245
}
221-
this._scroller.addWidget(nbdWidget);
246+
this._scroller.addWidget(this._nbdWidget);
222247
try {
223-
await nbdWidget.init();
248+
await this._nbdWidget.init();
224249

225250
Private.markUnchangedRanges(this._scroller.node, this._hasConflict);
226251
} catch (reason) {
@@ -306,6 +331,8 @@ export class NotebookDiff
306331
protected _model: Git.Diff.IModel<string>;
307332
protected _renderMime: IRenderMimeRegistry;
308333
protected _scroller: Panel;
334+
protected _nbdWidget: NotebookMergeWidget | NotebookDiffWidget;
335+
protected _lastSerializeModel: INotebookContent | null = null;
309336
}
310337

311338
namespace Private {
@@ -389,6 +416,9 @@ namespace Private {
389416
const UNCHANGED_CLASS = hasConflict
390417
? UNCHANGED_MERGE_CLASS
391418
: UNCHANGED_DIFF_CLASS;
419+
const NOTEBOOK_CLASS = hasConflict
420+
? '.jp-Notebook-merge'
421+
: '.jp-Notebook-diff';
392422

393423
const children = root.querySelectorAll(`.${CELL_CLASS}`);
394424
let rangeStart = -1;
@@ -416,10 +446,7 @@ namespace Private {
416446
if (rangeStart === 0) {
417447
// All elements were hidden, nothing to mark
418448
// Add info on root instead
419-
const tag =
420-
root.querySelector('.jp-Notebook-diff') ??
421-
root.querySelector('.jp-Notebook-merge') ??
422-
root;
449+
const tag = root.querySelector(NOTEBOOK_CLASS) ?? root;
423450
tag.setAttribute('data-nbdime-AllCellsHidden', N.toString());
424451
return;
425452
}

src/components/diff/PlainTextDiff.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Toolbar } from '@jupyterlab/apputils';
22
import { Mode } from '@jupyterlab/codemirror';
3+
import { Contents } from '@jupyterlab/services';
34
import { PromiseDelegate } from '@lumino/coreutils';
45
import { Widget } from '@lumino/widgets';
56
import { MergeView } from 'codemirror';
@@ -68,6 +69,13 @@ export class PlainTextDiff
6869
return this._model.hasConflict;
6970
}
7071

72+
/**
73+
* Checks if the conflicted file has been resolved.
74+
*/
75+
get isFileResolved(): boolean {
76+
return true;
77+
}
78+
7179
/**
7280
* Promise which fulfills when the widget is ready.
7381
*/
@@ -79,10 +87,14 @@ export class PlainTextDiff
7987
* Gets the file contents of a resolved merge conflict,
8088
* and rejects if unable to retrieve.
8189
*/
82-
getResolvedFile(): Promise<string> {
90+
getResolvedFile(): Promise<Partial<Contents.IModel>> {
8391
const value = this._mergeView?.editor().getValue() ?? null;
8492
if (value !== null) {
85-
return Promise.resolve(value);
93+
return Promise.resolve({
94+
type: 'file',
95+
format: 'text',
96+
content: value
97+
});
8698
} else {
8799
return Promise.reject('Failed to get a valid file value.');
88100
}

src/tokens.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Toolbar } from '@jupyterlab/apputils';
22
import { IChangedArgs } from '@jupyterlab/coreutils';
33
import { DocumentRegistry } from '@jupyterlab/docregistry';
4-
import { ServerConnection } from '@jupyterlab/services';
4+
import { Contents, ServerConnection } from '@jupyterlab/services';
55
import { JSONObject, ReadonlyJSONObject, Token } from '@lumino/coreutils';
66
import { IDisposable } from '@lumino/disposable';
77
import { ISignal } from '@lumino/signaling';
@@ -476,11 +476,15 @@ export namespace Git {
476476
* Note: Update the content and recompute the diff
477477
*/
478478
refresh(): Promise<void>;
479+
/**
480+
* Checks if the conflicted file has been resolved.
481+
*/
482+
isFileResolved: boolean;
479483
/**
480484
* Gets the file contents of a resolved merge conflict,
481485
* and rejects if unable to retrieve
482486
*/
483-
getResolvedFile(): Promise<T>;
487+
getResolvedFile(): Promise<Partial<Contents.IModel>>;
484488
}
485489

486490
/**
@@ -939,6 +943,12 @@ export namespace Git {
939943
super('Not in a Git Repository');
940944
}
941945
}
946+
947+
export class ConflictedFile extends Error {
948+
constructor(message: string) {
949+
super(message);
950+
}
951+
}
942952
}
943953

944954
/**

0 commit comments

Comments
 (0)