Skip to content

Commit e95e007

Browse files
authored
Merge pull request #691 from ianhi/slash-in-branch
FIx pushing when there are slashes in branch names or in remote name
2 parents 5056ca1 + aa5330b commit e95e007

File tree

4 files changed

+118
-74
lines changed

4 files changed

+118
-74
lines changed

jupyterlab_git/git.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -892,12 +892,12 @@ async def get_current_branch(self, current_path):
892892
to the `branch` command to get the name.
893893
See https://git-blame.blogspot.com/2013/06/checking-current-branch-programatically.html
894894
"""
895-
command = ["git", "symbolic-ref", "HEAD"]
895+
command = ["git", "symbolic-ref", "--short", "HEAD"]
896896
code, output, error = await execute(
897897
command, cwd=os.path.join(self.root_dir, current_path)
898898
)
899899
if code == 0:
900-
return output.split("/")[-1].strip()
900+
return output.strip()
901901
elif "not a symbolic ref" in error.lower():
902902
current_branch = await self._get_current_branch_detached(current_path)
903903
return current_branch
@@ -941,18 +941,22 @@ async def get_upstream_branch(self, current_path, branch_name):
941941
code, output, error = await execute(
942942
command, cwd=os.path.join(self.root_dir, current_path)
943943
)
944-
if code == 0:
945-
return output.strip()
946-
elif "fatal: no upstream configured for branch" in error.lower():
947-
return None
948-
elif "unknown revision or path not in the working tree" in error.lower():
949-
return None
950-
else:
951-
raise Exception(
952-
"Error [{}] occurred while executing [{}] command to get upstream branch.".format(
953-
error, " ".join(command)
954-
)
955-
)
944+
if code != 0:
945+
return {"code": code, "command": " ".join(command), "message": error}
946+
rev_parse_output = output.strip()
947+
948+
command = ["git", "config", "--local", "branch.{}.remote".format(branch_name)]
949+
code, output, error = await execute(
950+
command, cwd=os.path.join(self.root_dir, current_path)
951+
)
952+
if code != 0:
953+
return {"code": code, "command": " ".join(cmd), "message": error}
954+
955+
remote_name = output.strip()
956+
remote_branch = rev_parse_output.strip().lstrip(remote_name+"/")
957+
return {"code": code, "remote_short_name": remote_name, "remote_branch": remote_branch}
958+
959+
956960

957961
async def _get_tag(self, current_path, commit_sha):
958962
"""Execute 'git describe commit_sha' to get

jupyterlab_git/handlers.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,10 @@ async def post(self):
403403
"""
404404
current_path = self.get_json_body()["current_path"]
405405
current_branch = await self.git.get_current_branch(current_path)
406-
upstream = await self.git.get_upstream_branch(current_path, current_branch)
407-
self.finish(json.dumps({"upstream": upstream}))
406+
response = await self.git.get_upstream_branch(current_path, current_branch)
407+
if response['code'] != 0:
408+
self.set_status(500)
409+
self.finish(json.dumps(response))
408410

409411

410412
class GitPullHandler(GitHandler):
@@ -439,32 +441,28 @@ async def post(self):
439441
current_path = data["current_path"]
440442

441443
current_local_branch = await self.git.get_current_branch(current_path)
442-
current_upstream_branch = await self.git.get_upstream_branch(
444+
upstream = await self.git.get_upstream_branch(
443445
current_path, current_local_branch
444446
)
445447

446-
if current_upstream_branch and current_upstream_branch.strip():
447-
upstream = current_upstream_branch.split("/")
448-
if len(upstream) == 1:
449-
# If upstream is a local branch
450-
remote = "."
451-
branch = ":".join(["HEAD", upstream[0]])
452-
else:
453-
# If upstream is a remote branch
454-
remote = upstream[0]
455-
branch = ":".join(["HEAD", upstream[1]])
456-
448+
if upstream['code'] == 0:
449+
branch = ":".join(["HEAD", upstream['remote_branch']])
457450
response = await self.git.push(
458-
remote, branch, current_path, data.get("auth", None)
451+
upstream['remote_short_name'], branch, current_path, data.get("auth", None)
459452
)
460453

461454
else:
462-
response = {
463-
"code": 128,
464-
"message": "fatal: The current branch {} has no upstream branch.".format(
465-
current_local_branch
466-
),
467-
}
455+
if ("no upstream configured for branch" in upstream['message'].lower()
456+
or 'unknown revision or path' in upstream['message'].lower()):
457+
response = {
458+
"code": 128,
459+
"message": "fatal: The current branch {} has no upstream branch.".format(
460+
current_local_branch
461+
),
462+
}
463+
else:
464+
self.set_status(500)
465+
468466
self.finish(json.dumps(response))
469467

470468

jupyterlab_git/tests/test_branch.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def test_get_current_branch_success():
4444

4545
# Then
4646
mock_execute.assert_called_once_with(
47-
["git", "symbolic-ref", "HEAD"], cwd=os.path.join("/bin", "test_curr_path")
47+
["git", "symbolic-ref", "--short", "HEAD"], cwd=os.path.join("/bin", "test_curr_path")
4848
)
4949
assert "feature-foo" == actual_response
5050

@@ -337,11 +337,11 @@ async def test_get_current_branch_failure():
337337

338338
# Then
339339
mock_execute.assert_called_once_with(
340-
["git", "symbolic-ref", "HEAD"], cwd=os.path.join("/bin", "test_curr_path")
340+
["git", "symbolic-ref", "--short", "HEAD"], cwd=os.path.join("/bin", "test_curr_path")
341341
)
342342
assert (
343343
"Error [fatal: Not a git repository (or any of the parent directories): .git] "
344-
"occurred while executing [git symbolic-ref HEAD] command to get current branch."
344+
"occurred while executing [git symbolic-ref --short HEAD] command to get current branch."
345345
== str(error.value)
346346
)
347347

@@ -403,29 +403,42 @@ async def test_get_current_branch_detached_failure():
403403

404404
@pytest.mark.asyncio
405405
@pytest.mark.parametrize(
406-
"branch,upstream",
406+
"branch,upstream,remotename",
407407
[
408-
("feature-foo", "origin/master"),
409-
("master", "origin/master"),
410-
("feature-bar", "feature-foo"),
408+
("feature-foo", "master", "origin/withslash"),
409+
("master", "master", "origin"),
410+
("feature/bar", "feature-foo", ""),
411411
],
412412
)
413-
async def test_get_upstream_branch_success(branch, upstream):
413+
async def test_get_upstream_branch_success(branch, upstream, remotename):
414414
with patch("jupyterlab_git.git.execute") as mock_execute:
415415
# Given
416-
mock_execute.return_value = maybe_future((0, upstream, ""))
416+
mock_execute.side_effect = [
417+
maybe_future((0, remotename + '/' + upstream, '')),
418+
maybe_future((0, remotename, ''))
419+
]
417420

418421
# When
419422
actual_response = await Git(FakeContentManager("/bin")).get_upstream_branch(
420423
current_path="test_curr_path", branch_name=branch
421424
)
422425

423426
# Then
424-
mock_execute.assert_called_once_with(
425-
["git", "rev-parse", "--abbrev-ref", "{}@{{upstream}}".format(branch)],
426-
cwd=os.path.join("/bin", "test_curr_path"),
427+
mock_execute.assert_has_calls(
428+
[
429+
call(
430+
["git", "rev-parse", "--abbrev-ref", "{}@{{upstream}}".format(branch)],
431+
cwd=os.path.join("/bin", "test_curr_path"),
432+
),
433+
call(
434+
['git', 'config', '--local', 'branch.{}.remote'.format(branch)],
435+
cwd='/bin/test_curr_path',
436+
),
437+
438+
],
439+
any_order=False,
427440
)
428-
assert upstream == actual_response
441+
assert {'code': 0, 'remote_branch': upstream, 'remote_short_name': remotename} == actual_response
429442

430443

431444
@pytest.mark.asyncio
@@ -454,17 +467,12 @@ async def test_get_upstream_branch_failure(outputs, message):
454467
mock_execute.return_value = maybe_future(outputs)
455468

456469
# When
457-
if message:
458-
with pytest.raises(Exception) as error:
459-
await Git(FakeContentManager("/bin")).get_upstream_branch(
460-
current_path="test_curr_path", branch_name="blah"
461-
)
462-
assert message == str(error.value)
463-
else:
464-
response = await Git(FakeContentManager("/bin")).get_upstream_branch(
465-
current_path="test_curr_path", branch_name="blah"
466-
)
467-
assert response is None
470+
response = await Git(FakeContentManager("/bin")).get_upstream_branch(
471+
current_path="test_curr_path", branch_name="blah"
472+
)
473+
expected = {'code': 128, 'command': 'git rev-parse --abbrev-ref blah@{upstream}', 'message': outputs[2]}
474+
475+
assert response == expected
468476

469477
# Then
470478
mock_execute.assert_has_calls(
@@ -807,7 +815,7 @@ async def test_branch_success_detached_head():
807815
),
808816
# call to get current branch
809817
call(
810-
["git", "symbolic-ref", "HEAD"],
818+
["git", "symbolic-ref", "--short", "HEAD"],
811819
cwd=os.path.join("/bin", "test_curr_path"),
812820
),
813821
# call to get current branch name given a detached head

jupyterlab_git/tests/test_handlers.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,13 @@ class TestPush(ServerTest):
166166
@patch("jupyterlab_git.handlers.GitPushHandler.git", spec=Git)
167167
def test_push_handler_localbranch(self, mock_git):
168168
# Given
169-
mock_git.get_current_branch.return_value = maybe_future("foo")
169+
mock_git.get_current_branch.return_value = maybe_future("localbranch")
170170
mock_git.get_upstream_branch.return_value = maybe_future(
171-
"localbranch"
171+
{
172+
"code": 0,
173+
"remote_short_name": ".",
174+
"remote_branch": "localbranch"
175+
}
172176
)
173177
mock_git.push.return_value = maybe_future({"code": 0})
174178

@@ -178,7 +182,7 @@ def test_push_handler_localbranch(self, mock_git):
178182

179183
# Then
180184
mock_git.get_current_branch.assert_called_with("test_path")
181-
mock_git.get_upstream_branch.assert_called_with("test_path", "foo")
185+
mock_git.get_upstream_branch.assert_called_with("test_path", "localbranch")
182186
mock_git.push.assert_called_with(".", "HEAD:localbranch", "test_path", None)
183187

184188
assert response.status_code == 200
@@ -188,10 +192,11 @@ def test_push_handler_localbranch(self, mock_git):
188192
@patch("jupyterlab_git.handlers.GitPushHandler.git", spec=Git)
189193
def test_push_handler_remotebranch(self, mock_git):
190194
# Given
191-
mock_git.get_current_branch.return_value = maybe_future("foo")
192-
mock_git.get_upstream_branch.return_value = maybe_future(
193-
"origin/remotebranch"
194-
)
195+
mock_git.get_current_branch.return_value = maybe_future("foo/bar")
196+
upstream = {"code": 0,
197+
"remote_short_name": "origin/something",
198+
"remote_branch": "remote-branch-name"}
199+
mock_git.get_upstream_branch.return_value = maybe_future(upstream)
195200
mock_git.push.return_value = maybe_future({"code": 0})
196201

197202
# When
@@ -200,9 +205,9 @@ def test_push_handler_remotebranch(self, mock_git):
200205

201206
# Then
202207
mock_git.get_current_branch.assert_called_with("test_path")
203-
mock_git.get_upstream_branch.assert_called_with("test_path", "foo")
208+
mock_git.get_upstream_branch.assert_called_with("test_path", "foo/bar")
204209
mock_git.push.assert_called_with(
205-
"origin", "HEAD:remotebranch", "test_path", None
210+
"origin/something", "HEAD:remote-branch-name", "test_path", None
206211
)
207212

208213
assert response.status_code == 200
@@ -213,7 +218,12 @@ def test_push_handler_remotebranch(self, mock_git):
213218
def test_push_handler_noupstream(self, mock_git):
214219
# Given
215220
mock_git.get_current_branch.return_value = maybe_future("foo")
216-
mock_git.get_upstream_branch.return_value = maybe_future("")
221+
upstream = {
222+
"code": 128,
223+
"command": "",
224+
"message": "fatal: no upstream configured for branch 'foo'"
225+
}
226+
mock_git.get_upstream_branch.return_value = maybe_future(upstream)
217227
mock_git.push.return_value = maybe_future({"code": 0})
218228

219229
# When
@@ -234,23 +244,47 @@ def test_push_handler_noupstream(self, mock_git):
234244

235245

236246
class TestUpstream(ServerTest):
247+
@patch("jupyterlab_git.handlers.GitUpstreamHandler.git", spec=Git)
248+
def test_upstream_handler_forward_slashes(self, mock_git):
249+
# Given
250+
mock_git.get_current_branch.return_value = maybe_future("foo/bar")
251+
upstream = {"code": 0,
252+
"remote_short_name": "origin/something",
253+
"remote_branch": "foo/bar"}
254+
mock_git.get_upstream_branch.return_value = maybe_future(upstream)
255+
256+
# When
257+
body = {"current_path": "test_path"}
258+
response = self.tester.post(["upstream"], body=body)
259+
260+
# Then
261+
mock_git.get_current_branch.assert_called_with("test_path")
262+
mock_git.get_upstream_branch.assert_called_with("test_path", "foo/bar")
263+
264+
assert response.status_code == 200
265+
payload = response.json()
266+
assert payload == upstream
267+
237268
@patch("jupyterlab_git.handlers.GitUpstreamHandler.git", spec=Git)
238269
def test_upstream_handler_localbranch(self, mock_git):
239270
# Given
240-
mock_git.get_current_branch.return_value = maybe_future("foo")
241-
mock_git.get_upstream_branch.return_value = maybe_future("bar")
271+
mock_git.get_current_branch.return_value = maybe_future("foo/bar")
272+
upstream = {"code": 0,
273+
"remote_short_name": ".",
274+
"remote_branch": "foo/bar"}
275+
mock_git.get_upstream_branch.return_value = maybe_future(upstream)
242276

243277
# When
244278
body = {"current_path": "test_path"}
245279
response = self.tester.post(["upstream"], body=body)
246280

247281
# Then
248282
mock_git.get_current_branch.assert_called_with("test_path")
249-
mock_git.get_upstream_branch.assert_called_with("test_path", "foo")
283+
mock_git.get_upstream_branch.assert_called_with("test_path", "foo/bar")
250284

251285
assert response.status_code == 200
252286
payload = response.json()
253-
assert payload == {"upstream": "bar"}
287+
assert payload == upstream
254288

255289

256290
class TestDiffContent(ServerTest):

0 commit comments

Comments
 (0)