Skip to content

Commit 548a668

Browse files
ianhifcollonval
authored andcommitted
add changedFiles method to model
fix changed_files + create interface for result revert files to disk version on discard changes or switching branches modify open files on reverting a commit don't overwrite unsaved changes make changed_files more general you can always add the ^! to the commit id. This function isn't currently used anywhere except for this PR so changing this shouldn't break anything update open files on revert commit update tests with mock branches and mock docmanager There may be a better way to mock the docmanager but I can't figure it out add current_path to python tests
1 parent 7558e29 commit 548a668

File tree

6 files changed

+187
-17
lines changed

6 files changed

+187
-17
lines changed

jupyterlab_git/git.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async def config(self, top_repo_path, **kwargs):
184184

185185
return response
186186

187-
async def changed_files(self, base=None, remote=None, single_commit=None):
187+
async def changed_files(self, current_path, base=None, remote=None, single_commit=None):
188188
"""Gets the list of changed files between two Git refs, or the files changed in a single commit
189189
190190
There are two reserved "refs" for the base
@@ -204,7 +204,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
204204
}
205205
"""
206206
if single_commit:
207-
cmd = ["git", "diff", "{}^!".format(single_commit), "--name-only", "-z"]
207+
cmd = ["git", "diff", single_commit, "--name-only", "-z"]
208208
elif base and remote:
209209
if base == "WORKING":
210210
cmd = ["git", "diff", remote, "--name-only", "-z"]
@@ -219,7 +219,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
219219

220220
response = {}
221221
try:
222-
code, output, error = await execute(cmd, cwd=self.root_dir)
222+
code, output, error = await execute(cmd, cwd=os.path.join(self.root_dir, current_path))
223223
except subprocess.CalledProcessError as e:
224224
response["code"] = e.returncode
225225
response["message"] = e.output.decode("utf-8")

jupyterlab_git/tests/test_diff.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
async def test_changed_files_invalid_input():
1515
with pytest.raises(tornado.web.HTTPError):
1616
await Git(FakeContentManager("/bin")).changed_files(
17-
base="64950a634cd11d1a01ddfedaeffed67b531cb11e"
17+
current_path="test-path", base="64950a634cd11d1a01ddfedaeffed67b531cb11e"
1818
)
1919

2020

@@ -29,7 +29,7 @@ async def test_changed_files_single_commit():
2929

3030
# When
3131
actual_response = await Git(FakeContentManager("/bin")).changed_files(
32-
single_commit="64950a634cd11d1a01ddfedaeffed67b531cb11e"
32+
current_path="test-path", single_commit="64950a634cd11d1a01ddfedaeffed67b531cb11e^!"
3333
)
3434

3535
# Then
@@ -41,7 +41,7 @@ async def test_changed_files_single_commit():
4141
"--name-only",
4242
"-z",
4343
],
44-
cwd="/bin",
44+
cwd="/bin/test-path",
4545
)
4646
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
4747

@@ -56,12 +56,12 @@ async def test_changed_files_working_tree():
5656

5757
# When
5858
actual_response = await Git(FakeContentManager("/bin")).changed_files(
59-
base="WORKING", remote="HEAD"
59+
current_path="test-path", base="WORKING", remote="HEAD"
6060
)
6161

6262
# Then
6363
mock_execute.assert_called_once_with(
64-
["git", "diff", "HEAD", "--name-only", "-z"], cwd="/bin"
64+
["git", "diff", "HEAD", "--name-only", "-z"], cwd="/bin/test-path"
6565
)
6666
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
6767

@@ -76,12 +76,12 @@ async def test_changed_files_index():
7676

7777
# When
7878
actual_response = await Git(FakeContentManager("/bin")).changed_files(
79-
base="INDEX", remote="HEAD"
79+
current_path="test-path", base="INDEX", remote="HEAD"
8080
)
8181

8282
# Then
8383
mock_execute.assert_called_once_with(
84-
["git", "diff", "--staged", "HEAD", "--name-only", "-z"], cwd="/bin"
84+
["git", "diff", "--staged", "HEAD", "--name-only", "-z"], cwd="/bin/test-path"
8585
)
8686
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
8787

@@ -96,12 +96,12 @@ async def test_changed_files_two_commits():
9696

9797
# When
9898
actual_response = await Git(FakeContentManager("/bin")).changed_files(
99-
base="HEAD", remote="origin/HEAD"
99+
current_path = "test-path", base="HEAD", remote="origin/HEAD"
100100
)
101101

102102
# Then
103103
mock_execute.assert_called_once_with(
104-
["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin"
104+
["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin/test-path"
105105
)
106106
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
107107

@@ -114,12 +114,12 @@ async def test_changed_files_git_diff_error():
114114

115115
# When
116116
actual_response = await Git(FakeContentManager("/bin")).changed_files(
117-
base="HEAD", remote="origin/HEAD"
117+
current_path="test-path", base="HEAD", remote="origin/HEAD"
118118
)
119119

120120
# Then
121121
mock_execute.assert_called_once_with(
122-
["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin"
122+
["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin/test-path"
123123
)
124124
assert {"code": 128, "message": "error message"} == actual_response
125125

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from '@jupyterlab/application';
66
import { Dialog, showErrorMessage } from '@jupyterlab/apputils';
77
import { IChangedArgs } from '@jupyterlab/coreutils';
8+
import { IDocumentManager } from '@jupyterlab/docmanager';
89
import { FileBrowserModel, IFileBrowserFactory } from '@jupyterlab/filebrowser';
910
import { IMainMenu } from '@jupyterlab/mainmenu';
1011
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
@@ -32,6 +33,7 @@ const plugin: JupyterFrontEndPlugin<IGitExtension> = {
3233
IFileBrowserFactory,
3334
IRenderMimeRegistry,
3435
ISettingRegistry,
36+
IDocumentManager,
3537
IStatusBar
3638
],
3739
provides: IGitExtension,
@@ -54,6 +56,7 @@ async function activate(
5456
factory: IFileBrowserFactory,
5557
renderMime: IRenderMimeRegistry,
5658
settingRegistry: ISettingRegistry,
59+
docmanager: IDocumentManager,
5760
statusBar: IStatusBar
5861
): Promise<IGitExtension> {
5962
let gitExtension: GitExtension | null = null;
@@ -106,7 +109,12 @@ async function activate(
106109
return null;
107110
}
108111
// Create the Git model
109-
gitExtension = new GitExtension(serverSettings.serverRoot, app, settings);
112+
gitExtension = new GitExtension(
113+
serverSettings.serverRoot,
114+
app,
115+
docmanager,
116+
settings
117+
);
110118

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

src/model.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { JupyterFrontEnd } from '@jupyterlab/application';
22
import { IChangedArgs, PathExt } from '@jupyterlab/coreutils';
3+
import { IDocumentManager } from '@jupyterlab/docmanager';
34
import { ServerConnection } from '@jupyterlab/services';
45
import { ISettingRegistry } from '@jupyterlab/settingregistry';
56
import { LinkedList } from '@lumino/collections';
@@ -28,11 +29,13 @@ export class GitExtension implements IGitExtension {
2829
constructor(
2930
serverRoot: string,
3031
app: JupyterFrontEnd = null,
32+
docmanager: IDocumentManager = null,
3133
settings?: ISettingRegistry.ISettings
3234
) {
3335
const self = this;
3436
this._serverRoot = serverRoot;
3537
this._app = app;
38+
this._docmanager = docmanager;
3639
this._settings = settings || null;
3740

3841
let interval: number;
@@ -459,6 +462,20 @@ export class GitExtension implements IGitExtension {
459462
const tid = this._addTask('git:checkout');
460463
try {
461464
response = await httpGitRequest('/git/checkout', 'POST', body);
465+
466+
if (response.ok) {
467+
if (body.checkout_branch) {
468+
const files = (await this.changedFiles(
469+
this._currentBranch.name,
470+
body.branchname
471+
))['files'];
472+
if (files) {
473+
files.forEach(file => this._revertFile(file));
474+
}
475+
} else {
476+
this._revertFile(options.filename);
477+
}
478+
}
462479
} catch (err) {
463480
throw new ServerConnection.NetworkError(err);
464481
} finally {
@@ -468,6 +485,7 @@ export class GitExtension implements IGitExtension {
468485
if (!response.ok) {
469486
throw new ServerConnection.ResponseError(response, data.message);
470487
}
488+
471489
if (body.checkout_branch) {
472490
await this.refreshBranch();
473491
this._headChanged.emit();
@@ -609,11 +627,17 @@ export class GitExtension implements IGitExtension {
609627
return Promise.resolve(new Response(JSON.stringify(response)));
610628
}
611629
const tid = this._addTask('git:commit:revert');
630+
const files = (await this.changedFiles(null, null, hash + '^!'))['files'];
612631
try {
613632
response = await httpGitRequest('/git/delete_commit', 'POST', {
614633
commit_id: hash,
615634
top_repo_path: path
616635
});
636+
if (response.ok && files) {
637+
files.forEach(file => {
638+
this._revertFile(file);
639+
});
640+
}
617641
} catch (err) {
618642
throw new ServerConnection.NetworkError(err);
619643
} finally {
@@ -905,12 +929,29 @@ export class GitExtension implements IGitExtension {
905929
return Promise.resolve(new Response(JSON.stringify(response)));
906930
}
907931
const tid = this._addTask('git:reset:changes');
932+
const reset_all = filename === undefined;
933+
let files;
934+
if (reset_all) {
935+
files = (await this.changedFiles('INDEX', 'HEAD'))['files'];
936+
}
908937
try {
909938
response = await httpGitRequest('/git/reset', 'POST', {
910939
reset_all: filename === undefined,
911940
filename: filename === undefined ? null : filename,
912941
top_repo_path: path
913942
});
943+
944+
if (response.ok) {
945+
if (reset_all) {
946+
if (files) {
947+
files.forEach(file => {
948+
this._revertFile(file);
949+
});
950+
}
951+
} else {
952+
this._revertFile(filename);
953+
}
954+
}
914955
} catch (err) {
915956
throw new ServerConnection.NetworkError(err);
916957
} finally {
@@ -947,12 +988,20 @@ export class GitExtension implements IGitExtension {
947988
};
948989
return Promise.resolve(new Response(JSON.stringify(response)));
949990
}
991+
const files = (await this.changedFiles(null, null, hash))['files'];
950992
const tid = this._addTask('git:reset:hard');
951993
try {
952994
response = await httpGitRequest('/git/reset_to_commit', 'POST', {
953995
commit_id: hash,
954996
top_repo_path: path
955997
});
998+
if (response.ok) {
999+
if (files) {
1000+
files.forEach(file => {
1001+
this._revertFile(file);
1002+
});
1003+
}
1004+
}
9561005
} catch (err) {
9571006
throw new ServerConnection.NetworkError(err);
9581007
} finally {
@@ -1226,6 +1275,37 @@ export class GitExtension implements IGitExtension {
12261275
return Promise.resolve(response);
12271276
}
12281277

1278+
/**
1279+
* Get list of files changed between two commits or two branches
1280+
* @param base id of base commit or base branch for comparison
1281+
* @param remote id of remote commit or remote branch for comparison
1282+
* @param singleCommit id of a single commit
1283+
*
1284+
* @returns the names of the changed files
1285+
*/
1286+
async changedFiles(
1287+
base?: string,
1288+
remote?: string,
1289+
singleCommit?: string
1290+
): Promise<Git.IChangedFilesResult> {
1291+
try {
1292+
const response = await httpGitRequest('/git/changed_files', 'POST', {
1293+
current_path: this.pathRepository,
1294+
base: base,
1295+
remote: remote,
1296+
single_commit: singleCommit
1297+
});
1298+
if (!response.ok) {
1299+
return response.json().then((data: any) => {
1300+
throw new ServerConnection.ResponseError(response, data.message);
1301+
});
1302+
}
1303+
return response.json();
1304+
} catch (err) {
1305+
throw new ServerConnection.NetworkError(err);
1306+
}
1307+
}
1308+
12291309
/**
12301310
* Make request for a list of all git branches in the repository
12311311
* Retrieve a list of repository branches.
@@ -1344,12 +1424,26 @@ export class GitExtension implements IGitExtension {
13441424
return this._taskID;
13451425
}
13461426

1427+
/**
1428+
* if file is open in JupyterLab find the widget and ensure the JupyterLab
1429+
* version matches the version on disk. Do nothing if the file has unsaved changes
1430+
*
1431+
* @param path path to the file to be reverted
1432+
*/
1433+
private _revertFile(path: string): void {
1434+
const widget = this._docmanager.findWidget(this.getRelativeFilePath(path));
1435+
if (widget && !widget.context.model.dirty) {
1436+
widget.context.revert();
1437+
}
1438+
}
1439+
13471440
private _status: Git.IStatusFile[] = [];
13481441
private _pathRepository: string | null = null;
13491442
private _branches: Git.IBranch[];
13501443
private _currentBranch: Git.IBranch;
13511444
private _serverRoot: string;
13521445
private _app: JupyterFrontEnd | null;
1446+
private _docmanager: IDocumentManager | null;
13531447
private _diffProviders: { [key: string]: Git.IDiffCallback } = {};
13541448
private _isDisposed = false;
13551449
private _markerCache: Markers = new Markers(() => this._markChanged.emit());

src/tokens.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,15 @@ export namespace Git {
451451
files?: IStatusFileResult[];
452452
}
453453

454+
/** Interface for changed_files request result
455+
* lists the names of files that have differences between two commits
456+
* or beween two branches, or that were changed by a single commit
457+
*/
458+
export interface IChangedFilesResult {
459+
code: number;
460+
files?: string[];
461+
}
462+
454463
/** Interface for GitLog request result,
455464
* has the info of a single past commit
456465
*/

0 commit comments

Comments
 (0)