Skip to content

Commit 67d1896

Browse files
authored
Merge pull request #740 from fcollonval/auto-backport-of-pr-676-on-0.11.x
Backport PR #676: Update open files when Git commands modify them
2 parents a78b0c8 + a528bb7 commit 67d1896

File tree

6 files changed

+176
-17
lines changed

6 files changed

+176
-17
lines changed

jupyterlab_git/git.py

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

193193
return response
194194

195-
async def changed_files(self, base=None, remote=None, single_commit=None):
195+
async def changed_files(self, current_path, base=None, remote=None, single_commit=None):
196196
"""Gets the list of changed files between two Git refs, or the files changed in a single commit
197197
198198
There are two reserved "refs" for the base
@@ -212,7 +212,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
212212
}
213213
"""
214214
if single_commit:
215-
cmd = ["git", "diff", "{}^!".format(single_commit), "--name-only", "-z"]
215+
cmd = ["git", "diff", single_commit, "--name-only", "-z"]
216216
elif base and remote:
217217
if base == "WORKING":
218218
cmd = ["git", "diff", remote, "--name-only", "-z"]
@@ -227,7 +227,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
227227

228228
response = {}
229229
try:
230-
code, output, error = await execute(cmd, cwd=self.root_dir)
230+
code, output, error = await execute(cmd, cwd=os.path.join(self.root_dir, current_path))
231231
except subprocess.CalledProcessError as e:
232232
response["code"] = e.returncode
233233
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 { IChangedArgs, ISettingRegistry } from '@jupyterlab/coreutils';
77
import { Dialog, showErrorMessage } from '@jupyterlab/apputils';
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;
@@ -110,7 +113,12 @@ async function activate(
110113
return null;
111114
}
112115
// Create the Git model
113-
gitExtension = new GitExtension(serverSettings.serverRoot, app, settings);
116+
gitExtension = new GitExtension(
117+
serverSettings.serverRoot,
118+
app,
119+
docmanager,
120+
settings
121+
);
114122

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

src/model.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Poll,
66
ISettingRegistry
77
} from '@jupyterlab/coreutils';
8+
import { IDocumentManager } from '@jupyterlab/docmanager';
89
import { ServerConnection } from '@jupyterlab/services';
910
import { CommandRegistry } from '@phosphor/commands';
1011
import { JSONObject } from '@phosphor/coreutils';
@@ -31,11 +32,13 @@ export class GitExtension implements IGitExtension {
3132
constructor(
3233
serverRoot: string,
3334
app: JupyterFrontEnd = null,
35+
docmanager: IDocumentManager = null,
3436
settings?: ISettingRegistry.ISettings
3537
) {
3638
const self = this;
3739
this._serverRoot = serverRoot;
3840
this._app = app;
41+
this._docmanager = docmanager;
3942
this._settings = settings || null;
4043

4144
let interval: number;
@@ -462,6 +465,19 @@ export class GitExtension implements IGitExtension {
462465
const tid = this._addTask('git:checkout');
463466
try {
464467
response = await httpGitRequest('/git/checkout', 'POST', body);
468+
469+
if (response.ok) {
470+
if (body.checkout_branch) {
471+
const files = (
472+
await this.changedFiles(this._currentBranch.name, body.branchname)
473+
)['files'];
474+
if (files) {
475+
files.forEach(file => this._revertFile(file));
476+
}
477+
} else {
478+
this._revertFile(options.filename);
479+
}
480+
}
465481
} catch (err) {
466482
throw new ServerConnection.NetworkError(err);
467483
} finally {
@@ -471,6 +487,7 @@ export class GitExtension implements IGitExtension {
471487
if (!response.ok) {
472488
throw new ServerConnection.ResponseError(response, data.message);
473489
}
490+
474491
if (body.checkout_branch) {
475492
await this.refreshBranch();
476493
this._headChanged.emit();
@@ -612,11 +629,17 @@ export class GitExtension implements IGitExtension {
612629
return Promise.resolve(new Response(JSON.stringify(response)));
613630
}
614631
const tid = this._addTask('git:commit:revert');
632+
const files = (await this.changedFiles(null, null, hash + '^!'))['files'];
615633
try {
616634
response = await httpGitRequest('/git/delete_commit', 'POST', {
617635
commit_id: hash,
618636
top_repo_path: path
619637
});
638+
if (response.ok && files) {
639+
files.forEach(file => {
640+
this._revertFile(file);
641+
});
642+
}
620643
} catch (err) {
621644
throw new ServerConnection.NetworkError(err);
622645
} finally {
@@ -908,12 +931,29 @@ export class GitExtension implements IGitExtension {
908931
return Promise.resolve(new Response(JSON.stringify(response)));
909932
}
910933
const tid = this._addTask('git:reset:changes');
934+
const reset_all = filename === undefined;
935+
let files;
936+
if (reset_all) {
937+
files = (await this.changedFiles('INDEX', 'HEAD'))['files'];
938+
}
911939
try {
912940
response = await httpGitRequest('/git/reset', 'POST', {
913941
reset_all: filename === undefined,
914942
filename: filename === undefined ? null : filename,
915943
top_repo_path: path
916944
});
945+
946+
if (response.ok) {
947+
if (reset_all) {
948+
if (files) {
949+
files.forEach(file => {
950+
this._revertFile(file);
951+
});
952+
}
953+
} else {
954+
this._revertFile(filename);
955+
}
956+
}
917957
} catch (err) {
918958
throw new ServerConnection.NetworkError(err);
919959
} finally {
@@ -950,12 +990,20 @@ export class GitExtension implements IGitExtension {
950990
};
951991
return Promise.resolve(new Response(JSON.stringify(response)));
952992
}
993+
const files = (await this.changedFiles(null, null, hash))['files'];
953994
const tid = this._addTask('git:reset:hard');
954995
try {
955996
response = await httpGitRequest('/git/reset_to_commit', 'POST', {
956997
commit_id: hash,
957998
top_repo_path: path
958999
});
1000+
if (response.ok) {
1001+
if (files) {
1002+
files.forEach(file => {
1003+
this._revertFile(file);
1004+
});
1005+
}
1006+
}
9591007
} catch (err) {
9601008
throw new ServerConnection.NetworkError(err);
9611009
} finally {
@@ -1229,6 +1277,37 @@ export class GitExtension implements IGitExtension {
12291277
return Promise.resolve(response);
12301278
}
12311279

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

1429+
/**
1430+
* if file is open in JupyterLab find the widget and ensure the JupyterLab
1431+
* version matches the version on disk. Do nothing if the file has unsaved changes
1432+
*
1433+
* @param path path to the file to be reverted
1434+
*/
1435+
private _revertFile(path: string): void {
1436+
const widget = this._docmanager.findWidget(this.getRelativeFilePath(path));
1437+
if (widget && !widget.context.model.dirty) {
1438+
widget.context.revert();
1439+
}
1440+
}
1441+
13501442
private _status: Git.IStatusFile[] = [];
13511443
private _pathRepository: string | null = null;
13521444
private _branches: Git.IBranch[];
13531445
private _currentBranch: Git.IBranch;
13541446
private _serverRoot: string;
13551447
private _app: JupyterFrontEnd | null;
1448+
private _docmanager: IDocumentManager | null;
13561449
private _diffProviders: { [key: string]: Git.IDiffCallback } = {};
13571450
private _isDisposed = false;
13581451
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
@@ -448,6 +448,15 @@ export namespace Git {
448448
files?: IStatusFileResult[];
449449
}
450450

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

0 commit comments

Comments
 (0)