Skip to content

Commit 88acc03

Browse files
authored
Merge pull request #676 from ianhi/update-open-files
Update open files when Git commands modify them
2 parents 7558e29 + 57b90ec commit 88acc03

File tree

6 files changed

+177
-17
lines changed

6 files changed

+177
-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)