Skip to content

Commit 2bf889e

Browse files
authored
Merge pull request #469 from telamonian/fix_git_branch_edgecases
Fixes #460: better handling of branch edge cases
2 parents 095381a + 727ed5e commit 2bf889e

File tree

2 files changed

+36
-69
lines changed

2 files changed

+36
-69
lines changed

jupyterlab_git/git.py

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def branch_heads(self, current_path):
383383
"""
384384
Execute 'git for-each-ref' command on refs/heads & return the result.
385385
"""
386+
# Format reference: https://git-scm.com/docs/git-for-each-ref#_field_names
386387
formats = ['refname:short', 'objectname', 'upstream:short', 'HEAD']
387388
cmd = ["git", "for-each-ref", "--format=" + "%09".join("%({})".format(f) for f in formats), "refs/heads/"]
388389
p = subprocess.Popen(
@@ -397,7 +398,6 @@ def branch_heads(self, current_path):
397398
results = []
398399
try:
399400
for name,commit_sha,upstream_name,is_current_branch in (line.split('\t') for line in output.decode("utf-8").splitlines()):
400-
# Format reference : https://git-scm.com/docs/git-for-each-ref#_field_names
401401
is_current_branch = bool(is_current_branch.strip())
402402

403403
branch = {
@@ -412,15 +412,14 @@ def branch_heads(self, current_path):
412412
if is_current_branch:
413413
current_branch = branch
414414

415-
# Remote branch is seleted use 'git branch -a' as fallback machanism
416-
# to get add detached head on remote branch to preserve older functionality
417-
# TODO : Revisit this to checkout new local branch with same name as remote
418-
# when the remote branch is seleted, VS Code git does the same thing.
419-
if not current_branch and self.get_current_branch(current_path) == "HEAD":
415+
# Above can fail in certain cases, such as an empty repo with
416+
# no commits. In that case, just fall back to determining
417+
# current branch
418+
if not current_branch:
420419
branch = {
421420
"is_current_branch": True,
422421
"is_remote_branch": False,
423-
"name": self._get_detached_head_name(current_path),
422+
"name": self.get_current_branch(current_path),
424423
"upstream": None,
425424
"top_commit": None,
426425
"tag": None,
@@ -447,6 +446,7 @@ def branch_remotes(self, current_path):
447446
"""
448447
Execute 'git for-each-ref' command on refs/heads & return the result.
449448
"""
449+
# Format reference: https://git-scm.com/docs/git-for-each-ref#_field_names
450450
formats = ['refname:short', 'objectname']
451451
cmd = ["git", "for-each-ref", "--format=" + "%09".join("%({})".format(f) for f in formats), "refs/remotes/"]
452452
p = subprocess.Popen(
@@ -460,7 +460,6 @@ def branch_remotes(self, current_path):
460460
results = []
461461
try:
462462
for name,commit_sha in (line.split('\t') for line in output.decode("utf-8").splitlines()):
463-
# Format reference : https://git-scm.com/docs/git-for-each-ref#_field_names
464463
results.append({
465464
"is_current_branch": False,
466465
"is_remote_branch": True,
@@ -771,18 +770,6 @@ def init(self, current_path):
771770
)
772771
return my_output
773772

774-
def _is_branch(self, reference_name):
775-
"""Check if the given reference is a branch
776-
"""
777-
return reference_name.startswith("refs/heads/") or reference_name.startswith(
778-
"refs/remotes/"
779-
)
780-
781-
def _is_current_branch(self, branch_name, current_branch_name):
782-
"""Check if given branch is current branch
783-
"""
784-
return branch_name == current_branch_name
785-
786773
def _is_remote_branch(self, branch_reference):
787774
"""Check if given branch is remote branch by comparing with 'remotes/',
788775
TODO : Consider a better way to check remote branch
@@ -800,10 +787,12 @@ def _get_branch_name(self, branch_reference):
800787
raise ValueError("Reference [{}] is not a valid branch.", branch_reference)
801788

802789
def get_current_branch(self, current_path):
803-
"""Execute 'git rev-parse --abbrev-ref HEAD' to
804-
check if given branch is current branch
790+
"""Use `symbolic-ref` to get the current branch name. In case of
791+
failure, assume that the HEAD is currently detached, and fall back
792+
to the `branch` command to get the name.
793+
See https://git-blame.blogspot.com/2013/06/checking-current-branch-programatically.html
805794
"""
806-
command = ["git", "rev-parse", "--abbrev-ref", "HEAD"]
795+
command = ["git", "symbolic-ref", "HEAD"]
807796
p = subprocess.Popen(
808797
command,
809798
stdout=PIPE,
@@ -812,15 +801,17 @@ def get_current_branch(self, current_path):
812801
)
813802
output, error = p.communicate()
814803
if p.returncode == 0:
815-
return output.decode("utf-8").strip()
804+
return output.decode("utf-8").split('/')[-1].strip()
805+
elif "not a symbolic ref" in error.decode("utf-8").lower():
806+
return self._get_current_branch_detached(current_path)
816807
else:
817808
raise Exception(
818809
"Error [{}] occurred while executing [{}] command to get current branch.".format(
819810
error.decode("utf-8"), " ".join(command)
820811
)
821812
)
822813

823-
def _get_detached_head_name(self, current_path):
814+
def _get_current_branch_detached(self, current_path):
824815
"""Execute 'git branch -a' to get current branch details in case of detached HEAD
825816
"""
826817
command = ["git", "branch", "-a"]
@@ -901,4 +892,3 @@ def _get_tag(self, current_path, commit_sha):
901892
error.decode("utf-8"), " ".join(command)
902893
)
903894
)
904-

jupyterlab_git/tests/test_branch.py

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,6 @@
77
from jupyterlab_git.git import Git
88

99

10-
def test_is_branch():
11-
test_cases = [
12-
('refs/heads/feature-foo', True),
13-
('refs/heads/master', True),
14-
('refs/remotes/origin/feature-foo', True),
15-
('refs/remotes/origin/HEAD', True),
16-
('refs/stash', False),
17-
('refs/tags/v0.1.0', False),
18-
('refs/tags/[email protected]', False)
19-
]
20-
for test_case in test_cases:
21-
actual_response = Git(root_dir='/bin')._is_branch(test_case[0])
22-
assert test_case[1] == actual_response
23-
24-
25-
def test_is_current_branch():
26-
current_branch = 'feature-foo'
27-
test_cases = [
28-
('feature-foo', True),
29-
('master', False),
30-
('origin/feature-foo', False),
31-
('origin/HEAD', False)
32-
]
33-
for test_case in test_cases:
34-
actual_response = Git(root_dir='/bin')._is_current_branch(test_case[0], current_branch)
35-
assert test_case[1] == actual_response
36-
37-
3810
def test_is_remote_branch():
3911
test_cases = [
4012
('refs/heads/feature-foo', False),
@@ -89,7 +61,7 @@ def test_get_current_branch_success(mock_subproc_popen):
8961
# Then
9062
mock_subproc_popen.assert_has_calls([
9163
call(
92-
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
64+
['git', 'symbolic-ref', 'HEAD'],
9365
stdout=PIPE,
9466
stderr=PIPE,
9567
cwd='/bin/test_curr_path'
@@ -366,20 +338,20 @@ def test_get_current_branch_failure(mock_subproc_popen):
366338
# Then
367339
mock_subproc_popen.assert_has_calls([
368340
call(
369-
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
341+
['git', 'symbolic-ref', 'HEAD'],
370342
stdout=PIPE,
371343
stderr=PIPE,
372344
cwd='/bin/test_curr_path'
373345
),
374346
call().communicate()
375347
])
376348
assert 'Error [fatal: Not a git repository (or any of the parent directories): .git] ' \
377-
'occurred while executing [git rev-parse --abbrev-ref HEAD] command to get current branch.' == str(
349+
'occurred while executing [git symbolic-ref HEAD] command to get current branch.' == str(
378350
error.value)
379351

380352

381353
@patch('subprocess.Popen')
382-
def test_get_detached_head_name_success(mock_subproc_popen):
354+
def test_get_current_branch_detached_success(mock_subproc_popen):
383355
# Given
384356
process_output = [
385357
'* (HEAD detached at origin/feature-foo)',
@@ -396,7 +368,7 @@ def test_get_detached_head_name_success(mock_subproc_popen):
396368
mock_subproc_popen.return_value = process_mock
397369

398370
# When
399-
actual_response = Git(root_dir='/bin')._get_detached_head_name(
371+
actual_response = Git(root_dir='/bin')._get_current_branch_detached(
400372
current_path='test_curr_path')
401373

402374
# Then
@@ -413,7 +385,7 @@ def test_get_detached_head_name_success(mock_subproc_popen):
413385

414386

415387
@patch('subprocess.Popen')
416-
def test_get_detached_head_name_failure(mock_subproc_popen):
388+
def test_get_current_branch_detached_failure(mock_subproc_popen):
417389
# Given
418390
process_mock = Mock()
419391
attrs = {
@@ -426,7 +398,7 @@ def test_get_detached_head_name_failure(mock_subproc_popen):
426398

427399
# When
428400
with pytest.raises(Exception) as error:
429-
Git(root_dir='/bin')._get_detached_head_name(current_path='test_curr_path')
401+
Git(root_dir='/bin')._get_current_branch_detached(current_path='test_curr_path')
430402

431403
# Then
432404
mock_subproc_popen.assert_has_calls([
@@ -798,19 +770,23 @@ def test_branch_success_detached_head(mock_subproc_popen):
798770
' master',
799771
' remotes/origin/feature-foo'
800772
]
801-
process_mock = Mock(returncode=0)
802-
process_mock.communicate.side_effect = [
773+
774+
process_mock = Mock()
775+
com_returncodes = [0, 128, 0, 0]
776+
com_returns = [
803777
# Response for get all refs/heads
804778
('\n'.join(process_output_heads).encode('utf-8'), ''.encode('utf-8')),
805-
806779
# Response for get current branch
807-
('HEAD'.encode('utf-8'), ''.encode('utf-8')),
808-
# Responses for detached head name
780+
(''.encode('utf-8'), 'fatal: ref HEAD is not a symbolic ref'.encode('utf-8')),
781+
# Response for get current branch detached
809782
('\n'.join(detached_head_output).encode('utf-8'), ''.encode('utf-8')),
810-
811783
# Response for get all refs/remotes
812784
('\n'.join(process_output_remotes).encode('utf-8'), ''.encode('utf-8')),
813785
]
786+
def com_mock_side_effect():
787+
process_mock.returncode = com_returncodes.pop(0)
788+
return com_returns.pop(0)
789+
process_mock.communicate.side_effect = com_mock_side_effect
814790
mock_subproc_popen.return_value = process_mock
815791

816792
expected_response = {
@@ -868,13 +844,14 @@ def test_branch_success_detached_head(mock_subproc_popen):
868844

869845
# call to get current branch
870846
call(
871-
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
847+
['git', 'symbolic-ref', 'HEAD'],
872848
stdout=PIPE,
873849
stderr=PIPE,
874850
cwd='/bin/test_curr_path'
875851
),
876852
call().communicate(),
877-
# call to get detached head name
853+
854+
# call to get current branch name given a detached head
878855
call(
879856
['git', 'branch', '-a'],
880857
stdout=PIPE,

0 commit comments

Comments
 (0)