Skip to content

Commit 87b3bc0

Browse files
authored
Merge pull request #541 from kgryte/cancel-merge
Add setting support for canceling a pull if merge conflict
2 parents 8189278 + f05a214 commit 87b3bc0

File tree

8 files changed

+84
-12
lines changed

8 files changed

+84
-12
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyterlab-git/master?urlpath=lab/tree/examples/demo.ipynb) [![Build Status](https://travis-ci.org/jupyterlab/jupyterlab-git.svg?branch=master)](https://travis-ci.org/jupyterlab/jupyterlab-git) [![Version](https://img.shields.io/npm/v/@jupyterlab/git.svg)](https://www.npmjs.com/package/@jupyterlab/git) [![Version](https://img.shields.io/pypi/v/jupyterlab-git.svg)](https://pypi.org/project/jupyterlab-git/) [![Downloads](https://img.shields.io/npm/dm/@jupyterlab/git.svg)](https://www.npmjs.com/package/@jupyterlab/git) [![Version](https://img.shields.io/conda/vn/conda-forge/jupyterlab-git.svg)](https://anaconda.org/conda-forge/jupyterlab-git) [![Downloads](https://img.shields.io/conda/dn/conda-forge/jupyterlab-git.svg)](https://anaconda.org/conda-forge/jupyterlab-git)
44

5-
A JupyterLab extension for version control using git
5+
A JupyterLab extension for version control using Git
66

77
![](http://g.recordit.co/N9Ikzbyk8P.gif)
88

@@ -11,10 +11,11 @@ To see the extension in action, open the example notebook included in the Binder
1111
## Prerequisites
1212

1313
- JupyterLab
14+
- Git (version `>=1.7.4`)
1415

1516
## Usage
1617

17-
- Open the git extension from the _Git_ tab on the left panel
18+
- Open the Git extension from the _Git_ tab on the left panel
1819

1920
## Install
2021

jupyterlab_git/git.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ async def call_subprocess_with_authentication(
6767

6868
returncode = p.wait()
6969
p.close()
70-
7170
return returncode, "", response
7271
except pexpect.exceptions.EOF: # In case of pexpect failure
7372
response = p.before
@@ -740,15 +739,15 @@ async def commit(self, commit_msg, top_repo_path):
740739
return {"code": code, "command": " ".join(cmd), "message": error}
741740
return {"code": code}
742741

743-
async def pull(self, curr_fb_path, auth=None):
742+
async def pull(self, curr_fb_path, auth=None, cancel_on_conflict=False):
744743
"""
745744
Execute git pull --no-commit. Disables prompts for the password to avoid the terminal hanging while waiting
746745
for auth.
747746
"""
748747
env = os.environ.copy()
749748
if auth:
750749
env["GIT_TERMINAL_PROMPT"] = "1"
751-
code, _, error = await execute(
750+
code, output, error = await execute(
752751
["git", "pull", "--no-commit"],
753752
username=auth["username"],
754753
password=auth["password"],
@@ -757,7 +756,7 @@ async def pull(self, curr_fb_path, auth=None):
757756
)
758757
else:
759758
env["GIT_TERMINAL_PROMPT"] = "0"
760-
code, _, error = await execute(
759+
code, output, error = await execute(
761760
["git", "pull", "--no-commit"],
762761
env=env,
763762
cwd=os.path.join(self.root_dir, curr_fb_path),
@@ -766,7 +765,21 @@ async def pull(self, curr_fb_path, auth=None):
766765
response = {"code": code}
767766

768767
if code != 0:
769-
response["message"] = error.strip()
768+
output = output.strip()
769+
has_conflict = "automatic merge failed; fix conflicts and then commit the result." in output.lower()
770+
if cancel_on_conflict and has_conflict:
771+
code, _, error = await execute(
772+
["git", "merge", "--abort"],
773+
cwd=os.path.join(self.root_dir, curr_fb_path)
774+
)
775+
if code == 0:
776+
response["message"] = "Unable to pull latest changes as doing so would result in a merge conflict. In order to push your local changes, you may want to consider creating a new branch based on your current work and pushing the new branch. Provided your repository is hosted (e.g., on GitHub), once pushed, you can create a pull request against the original branch on the remote repository and manually resolve the conflicts during pull request review."
777+
else:
778+
response["message"] = error.strip()
779+
elif has_conflict:
780+
response["message"] = output
781+
else:
782+
response["message"] = error.strip()
770783

771784
return response
772785

jupyterlab_git/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ async def post(self):
416416
POST request handler, pulls files from a remote branch to your current branch.
417417
"""
418418
data = self.get_json_body()
419-
response = await self.git.pull(data["current_path"], data.get("auth", None))
419+
response = await self.git.pull(data["current_path"], data.get("auth", None), data.get("cancel_on_conflict", False))
420420

421421
self.finish(json.dumps(response))
422422

jupyterlab_git/tests/test_clone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,4 @@ async def test_git_clone_with_auth_auth_failure_from_git():
154154
assert {
155155
"code": 128,
156156
"message": "remote: Invalid username or password.\r\nfatal: Authentication failed for 'ghjkhjkl'",
157-
} == actual_response
157+
} == actual_response

jupyterlab_git/tests/test_pushpull.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ async def test_git_pull_fail():
3232
assert {"code": 1, "message": "Authentication failed"} == actual_response
3333

3434

35+
@pytest.mark.asyncio
36+
async def test_git_pull_with_conflict_fail():
37+
with patch("os.environ", {"TEST": "test"}):
38+
with patch("jupyterlab_git.git.execute") as mock_execute:
39+
# Given
40+
mock_execute.return_value = tornado.gen.maybe_future((1, "", "Automatic merge failed; fix conflicts and then commit the result."))
41+
42+
# When
43+
actual_response = await Git(FakeContentManager("/bin")).pull("test_curr_path")
44+
45+
# Then
46+
mock_execute.assert_has_calls([
47+
call(
48+
["git", "pull", "--no-commit"],
49+
cwd="/bin/test_curr_path",
50+
env={"TEST": "test", "GIT_TERMINAL_PROMPT": "0"},
51+
)
52+
]);
53+
assert {"code": 1, "message": "Automatic merge failed; fix conflicts and then commit the result."} == actual_response
54+
55+
3556
@pytest.mark.asyncio
3657
async def test_git_pull_with_auth_fail():
3758
with patch("os.environ", {"TEST": "test"}):
@@ -115,8 +136,34 @@ async def test_git_pull_with_auth_success():
115136

116137

117138
@pytest.mark.asyncio
118-
async def test_git_push_fail():
139+
async def test_git_pull_with_auth_success_and_conflict_fail():
140+
with patch("os.environ", {"TEST": "test"}):
141+
with patch("jupyterlab_git.git.execute") as mock_execute_with_authentication:
142+
# Given
143+
mock_execute_with_authentication.return_value = tornado.gen.maybe_future((1, "output", "Automatic merge failed; fix conflicts and then commit the result."))
144+
145+
# When
146+
auth = {
147+
"username" : "asdf",
148+
"password" : "qwerty"
149+
}
150+
actual_response = await Git(FakeContentManager("/bin")).pull("test_curr_path", auth)
151+
152+
# Then
153+
mock_execute_with_authentication.assert_has_calls([
154+
call(
155+
["git", "pull", "--no-commit"],
156+
cwd="/bin/test_curr_path",
157+
env={"TEST": "test", "GIT_TERMINAL_PROMPT": "1"},
158+
username="asdf",
159+
password="qwerty"
160+
)
161+
])
162+
assert {"code": 1, "message": "Automatic merge failed; fix conflicts and then commit the result."} == actual_response
163+
119164

165+
@pytest.mark.asyncio
166+
async def test_git_push_fail():
120167
with patch("os.environ", {"TEST": "test"}):
121168
with patch("jupyterlab_git.git.execute") as mock_execute:
122169
# Given
@@ -221,4 +268,3 @@ async def test_git_push_with_auth_success():
221268
env={"TEST": "test", "GIT_TERMINAL_PROMPT": "1"},
222269
)
223270
assert {"code": 0} == actual_response
224-

schema/plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"description": "jupyterlab-git settings.",
66
"type": "object",
77
"properties": {
8+
"cancelPullMergeConflict": {
9+
"type": "boolean",
10+
"title": "Cancel pull merge conflict",
11+
"description": "If true, when fetching and integrating changes from a remote repository, a conflicting merge is canceled and the working tree left untouched.",
12+
"default": false
13+
},
814
"disableBranchWithChanges": {
915
"type": "boolean",
1016
"title": "Disable branch with changes",

src/model.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class GitExtension implements IGitExtension {
2424
) {
2525
const model = this;
2626
this._app = app;
27+
this._settings = settings || null;
2728

2829
// Load the server root path
2930
this._getServerRoot()
@@ -742,7 +743,10 @@ export class GitExtension implements IGitExtension {
742743
try {
743744
let obj: Git.IPushPull = {
744745
current_path: path,
745-
auth
746+
auth,
747+
cancel_on_conflict: this._settings
748+
? (this._settings.composite['cancelPullMergeConflict'] as boolean)
749+
: false
746750
};
747751

748752
let response = await httpGitRequest('/git/pull', 'POST', obj);
@@ -1038,6 +1042,7 @@ export class GitExtension implements IGitExtension {
10381042
private _readyPromise: Promise<void> = Promise.resolve();
10391043
private _pendingReadyPromise = 0;
10401044
private _poll: Poll;
1045+
private _settings: ISettingRegistry.ISettings | null;
10411046
private _headChanged = new Signal<IGitExtension, void>(this);
10421047
private _markChanged = new Signal<IGitExtension, void>(this);
10431048
private _repositoryChanged = new Signal<

src/tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ export namespace Git {
460460
export interface IPushPull {
461461
current_path: string;
462462
auth?: IAuth;
463+
cancel_on_conflict?: boolean;
463464
}
464465

465466
/**

0 commit comments

Comments
 (0)