Skip to content

Commit 50952c9

Browse files
nibheisfcollonval
authored andcommitted
Fix remote checkout (#385)
* Fix remote checkout: no longer dettached (using the --track option) * Small fixes in checkout_branch(...) * add tests for _get_branch_reference() and checkout_branch() methods + documentation * Fix typoes + add more tests for mock__get_branch_reference + use .format() for string formatting. All tests have passed.
1 parent 98a6cc9 commit 50952c9

File tree

2 files changed

+281
-8
lines changed

2 files changed

+281
-8
lines changed

jupyterlab_git/git.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -609,25 +609,50 @@ def checkout_new_branch(self, branchname, current_path):
609609
"message": my_error.decode("utf-8"),
610610
}
611611

612+
def _get_branch_reference(self, branchname, current_path):
613+
"""
614+
Execute git rev-parse --symbolic-full-name <branch-name> and return the result (or None).
615+
"""
616+
p = subprocess.Popen(
617+
["git", "rev-parse", "--symbolic-full-name", branchname],
618+
stdout=PIPE,
619+
stderr=PIPE,
620+
cwd=os.path.join(self.root_dir, current_path),
621+
)
622+
my_output, my_error = p.communicate()
623+
if p.returncode == 0:
624+
return my_output.decode("utf-8").strip("\n")
625+
else:
626+
return None
627+
612628
def checkout_branch(self, branchname, current_path):
613629
"""
614630
Execute git checkout <branch-name> command & return the result.
631+
Use the --track parameter for a remote branch.
615632
"""
616-
p = Popen(
617-
["git", "checkout", branchname],
633+
reference_name = self._get_branch_reference(branchname, current_path)
634+
if reference_name is None:
635+
is_remote_branch = False
636+
else:
637+
is_remote_branch = self._is_remote_branch(reference_name)
638+
639+
if is_remote_branch:
640+
cmd = ["git", "checkout", "--track", branchname]
641+
else:
642+
cmd = ["git", "checkout", branchname]
643+
644+
p = subprocess.Popen(
645+
cmd,
618646
stdout=PIPE,
619647
stderr=PIPE,
620648
cwd=os.path.join(self.root_dir, current_path),
621649
)
650+
622651
my_output, my_error = p.communicate()
623652
if p.returncode == 0:
624-
return {"code": p.returncode, "message": my_output.decode("utf-8")}
653+
return { "code": 0, "message": my_output.decode("utf-8") }
625654
else:
626-
return {
627-
"code": p.returncode,
628-
"command": "git checkout " + branchname,
629-
"message": my_error.decode("utf-8"),
630-
}
655+
return { "code": p.returncode, "message": my_error.decode("utf-8"), "command": " ".join(cmd) }
631656

632657
def checkout(self, filename, top_repo_path):
633658
"""

jupyterlab_git/tests/test_branch.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,254 @@ def test_get_current_branch_success(mock_subproc_popen):
9898
])
9999
assert 'feature-foo' == actual_response
100100

101+
@patch('subprocess.Popen')
102+
@patch.object(Git, '_get_branch_reference', return_value=None)
103+
def test_checkout_branch_noref_success(mock__get_branch_reference, mock_subproc_popen):
104+
branch='test-branch'
105+
curr_path='test_curr_path'
106+
stdout_message='checkout output from git'
107+
stderr_message=''
108+
rc=0
109+
110+
# Given
111+
process_mock = Mock()
112+
attrs = {
113+
'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')),
114+
'returncode': rc
115+
}
116+
process_mock.configure_mock(**attrs)
117+
mock_subproc_popen.return_value = process_mock
118+
119+
# When
120+
actual_response = Git(root_dir='/bin').checkout_branch(branchname=branch, current_path=curr_path)
121+
122+
# Then
123+
mock__get_branch_reference.assert_has_calls([ call(branch, curr_path) ])
124+
125+
cmd=['git', 'checkout', branch]
126+
mock_subproc_popen.assert_has_calls([
127+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/{}'.format(curr_path)),
128+
call().communicate()
129+
])
130+
131+
assert { "code": rc, "message": stdout_message } == actual_response
132+
133+
134+
@patch('subprocess.Popen')
135+
@patch.object(Git, '_get_branch_reference', return_value=None)
136+
def test_checkout_branch_noref_failure(mock__get_branch_reference, mock_subproc_popen):
137+
branch='test-branch'
138+
curr_path='test_curr_path'
139+
stdout_message=''
140+
stderr_message="error: pathspec '{}' did not match any file(s) known to git".format(branch)
141+
rc=1
142+
143+
# Given
144+
process_mock = Mock()
145+
attrs = { 'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')), 'returncode': rc }
146+
process_mock.configure_mock(**attrs)
147+
mock_subproc_popen.return_value = process_mock
148+
149+
# When
150+
actual_response = Git(root_dir='/bin').checkout_branch(branchname=branch, current_path=curr_path)
151+
152+
# Then
153+
mock__get_branch_reference.assert_has_calls([ call(branch, curr_path) ])
154+
155+
cmd=['git', 'checkout', branch]
156+
mock_subproc_popen.assert_has_calls([
157+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/{}'.format(curr_path)),
158+
call().communicate()
159+
])
160+
161+
assert { "code": rc, "message": stderr_message, "command": ' '.join(cmd) } == actual_response
162+
163+
164+
@patch('subprocess.Popen')
165+
@patch.object(Git, '_get_branch_reference', return_value="refs/remotes/remote_branch")
166+
def test_checkout_branch_remoteref_success(mock__get_branch_reference, mock_subproc_popen):
167+
branch='test-branch'
168+
curr_path='test_curr_path'
169+
stdout_message='checkout output from git'
170+
stderr_message=''
171+
rc=0
172+
173+
# Given
174+
process_mock = Mock()
175+
attrs = {
176+
'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')),
177+
'returncode': rc
178+
}
179+
process_mock.configure_mock(**attrs)
180+
mock_subproc_popen.return_value = process_mock
181+
182+
# When
183+
actual_response = Git(root_dir='/bin').checkout_branch(branchname=branch, current_path=curr_path)
184+
185+
# Then
186+
mock__get_branch_reference.assert_has_calls([ call(branch, curr_path) ])
187+
188+
cmd=['git', 'checkout', '--track', branch]
189+
mock_subproc_popen.assert_has_calls([
190+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/{}'.format(curr_path)),
191+
call().communicate()
192+
])
193+
assert { "code": rc, "message": stdout_message } == actual_response
194+
195+
196+
@patch('subprocess.Popen')
197+
@patch.object(Git, '_get_branch_reference', return_value="refs/heads/local_branch")
198+
def test_checkout_branch_headsref_failure(mock__get_branch_reference, mock_subproc_popen):
199+
branch='test-branch'
200+
curr_path='test_curr_path'
201+
stdout_message=''
202+
stderr_message="error: pathspec '{}' did not match any file(s) known to git".format(branch)
203+
rc=1
204+
205+
# Given
206+
process_mock = Mock()
207+
attrs = { 'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')), 'returncode': rc }
208+
process_mock.configure_mock(**attrs)
209+
mock_subproc_popen.return_value = process_mock
210+
211+
# When
212+
actual_response = Git(root_dir='/bin').checkout_branch(branchname=branch, current_path=curr_path)
213+
214+
# Then
215+
mock__get_branch_reference.assert_has_calls([ call(branch, curr_path) ])
216+
217+
cmd=['git', 'checkout', branch]
218+
mock_subproc_popen.assert_has_calls([
219+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/{}'.format(curr_path)),
220+
call().communicate()
221+
])
222+
assert { "code": rc, "message": stderr_message, "command": ' '.join(cmd) } == actual_response
223+
224+
225+
@patch('subprocess.Popen')
226+
@patch.object(Git, '_get_branch_reference', return_value="refs/heads/local_branch")
227+
def test_checkout_branch_headsref_success(mock__get_branch_reference, mock_subproc_popen):
228+
branch='test-branch'
229+
stdout_message='checkout output from git'
230+
stderr_message=''
231+
rc=0
232+
233+
# Given
234+
process_mock = Mock()
235+
attrs = {
236+
'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')),
237+
'returncode': rc
238+
}
239+
process_mock.configure_mock(**attrs)
240+
mock_subproc_popen.return_value = process_mock
241+
242+
# When
243+
actual_response = Git(root_dir='/bin').checkout_branch(
244+
branchname=branch,
245+
current_path='test_curr_path')
246+
247+
# Then
248+
cmd=['git', 'checkout', branch]
249+
mock_subproc_popen.assert_has_calls([
250+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/test_curr_path'),
251+
call().communicate()
252+
])
253+
assert { "code": rc, "message": stdout_message } == actual_response
254+
255+
256+
@patch('subprocess.Popen')
257+
@patch.object(Git, '_get_branch_reference', return_value="refs/remotes/remote_branch")
258+
def test_checkout_branch_remoteref_failure(mock__get_branch_reference, mock_subproc_popen):
259+
branch='test-branch'
260+
stdout_message=''
261+
stderr_message="error: pathspec '{}' did not match any file(s) known to git".format(branch)
262+
rc=1
263+
264+
# Given
265+
process_mock = Mock()
266+
attrs = { 'communicate.return_value': (stdout_message.encode('utf-8'), stderr_message.encode('utf-8')), 'returncode': rc }
267+
process_mock.configure_mock(**attrs)
268+
mock_subproc_popen.return_value = process_mock
269+
270+
# When
271+
actual_response = Git(root_dir='/bin').checkout_branch(branchname=branch, current_path='test_curr_path')
272+
273+
# Then
274+
cmd=['git', 'checkout', '--track', branch]
275+
mock_subproc_popen.assert_has_calls([
276+
call(cmd, stdout=PIPE, stderr=PIPE, cwd='/bin/test_curr_path'),
277+
call().communicate()
278+
])
279+
assert { "code": rc, "message": stderr_message, "command": ' '.join(cmd) } == actual_response
280+
281+
282+
283+
@patch('subprocess.Popen')
284+
def test_get_branch_reference_success(mock_subproc_popen):
285+
actual_response = 0
286+
branch='test-branch'
287+
reference = 'refs/remotes/origin/test_branch'
288+
# Given
289+
process_mock = Mock()
290+
attrs = {
291+
'communicate.return_value': (reference.encode('utf-8'), ''.encode('utf-8')),
292+
'returncode': 0
293+
}
294+
process_mock.configure_mock(**attrs)
295+
mock_subproc_popen.return_value = process_mock
296+
297+
# When
298+
actual_response = Git(root_dir='/bin')._get_branch_reference(
299+
branchname=branch,
300+
current_path='test_curr_path')
301+
302+
# Then
303+
mock_subproc_popen.assert_has_calls([
304+
call(
305+
['git', 'rev-parse', '--symbolic-full-name', branch],
306+
stdout=PIPE,
307+
stderr=PIPE,
308+
cwd='/bin/test_curr_path'
309+
),
310+
call().communicate()
311+
])
312+
assert actual_response == reference
313+
314+
315+
@patch('subprocess.Popen')
316+
def test_get_branch_reference_failure(mock_subproc_popen):
317+
actual_response = 0
318+
branch='test-branch'
319+
reference = 'test-branch'
320+
# Given
321+
process_mock = Mock()
322+
attrs = {
323+
'communicate.return_value': (
324+
reference.encode('utf-8'),
325+
"fatal: ambiguous argument '{}': unknown revision or path not in the working tree.".format(branch).encode('utf-8')
326+
),
327+
'returncode': 128
328+
}
329+
process_mock.configure_mock(**attrs)
330+
mock_subproc_popen.return_value = process_mock
331+
332+
# When
333+
actual_response = Git(root_dir='/bin')._get_branch_reference(
334+
branchname=branch,
335+
current_path='test_curr_path')
336+
337+
# Then
338+
mock_subproc_popen.assert_has_calls([
339+
call(
340+
['git', 'rev-parse', '--symbolic-full-name', branch],
341+
stdout=PIPE,
342+
stderr=PIPE,
343+
cwd='/bin/test_curr_path'
344+
),
345+
call().communicate()
346+
])
347+
assert actual_response is None
348+
101349

102350
@patch('subprocess.Popen')
103351
def test_get_current_branch_failure(mock_subproc_popen):

0 commit comments

Comments
 (0)