Skip to content

Commit eceb16d

Browse files
jpichonmergify[bot]
authored andcommitted
Add option to checkout newly created branches
Additionally, fix an inconsistency where the create() function would leave the branch checked out when performing a hard reset, but not for newly created branches or for existing branches that didn't get reset.
1 parent 388a536 commit eceb16d

File tree

3 files changed

+158
-5
lines changed

3 files changed

+158
-5
lines changed

git_wrapper/branch.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _run_cherry(self, upstream, head, regex):
4949
return ret_data
5050

5151
@reference_exists("start_ref")
52-
def create(self, name, start_ref, reset_if_exists=False):
52+
def create(self, name, start_ref, reset_if_exists=False, checkout=False):
5353
"""Create a local branch based on start_ref.
5454
5555
If the branch exists, do nothing or hard reset it if reset_if_exists
@@ -60,14 +60,20 @@ def create(self, name, start_ref, reset_if_exists=False):
6060
a starting point.
6161
:param bool reset_if_exists: Whether to hard reset the branch to
6262
start_ref if the branch already exists.
63+
:param bool checkout: Whether to checkout the new branch
64+
:return: True if a new branch was created, None otherwise
6365
"""
6466
if not self.exists(name):
6567
self.git_repo.git.branch(name, start_ref)
68+
if checkout:
69+
self.git_repo.git.checkout(name)
6670
return True
67-
if self.exists(name) and not reset_if_exists:
68-
return
71+
6972
if self.exists(name) and reset_if_exists:
70-
self.hard_reset_to_ref(name, start_ref)
73+
self.hard_reset_to_ref(name, start_ref, checkout)
74+
75+
if checkout:
76+
self.git_repo.git.checkout(name)
7177

7278
def exists(self, name, remote=None):
7379
"""Checks if a branch exists locally or on the specified remote.
@@ -313,11 +319,12 @@ def hard_reset(self, branch="master", remote="origin",
313319
remote_ref = "{0}/{1}".format(remote, remote_branch)
314320
self.hard_reset_to_ref(branch, remote_ref)
315321

316-
def hard_reset_to_ref(self, branch, ref):
322+
def hard_reset_to_ref(self, branch, ref, checkout=True):
317323
"""Perform a hard reset of a local branch to any reference.
318324
319325
:param str branch: Local branch to reset
320326
:param str ref: Reference (commit, tag, ...) to reset to
327+
:param bool checkout: Whether to checkout the new branch
321328
"""
322329
# Ensure the reference maps to a commit
323330
try:
@@ -326,6 +333,13 @@ def hard_reset_to_ref(self, branch, ref):
326333
msg = "Could not find reference {0}.".format(ref)
327334
raise_from(exceptions.ReferenceNotFoundException(msg), ex)
328335

336+
try:
337+
# Preserve the reference name if there is one
338+
orig = self.git_repo.repo.head.ref.name
339+
except TypeError:
340+
# Detached head
341+
orig = self.git_repo.repo.head.commit.hexsha
342+
329343
# Switch to the branch
330344
try:
331345
self.git_repo.git.checkout(branch)
@@ -348,6 +362,17 @@ def hard_reset_to_ref(self, branch, ref):
348362
)
349363
raise_from(exceptions.ResetException(msg), ex)
350364

365+
# Return to the original head if required
366+
if not checkout:
367+
try:
368+
self.git_repo.git.checkout(orig)
369+
except git.GitCommandError as ex:
370+
msg = (
371+
"Could not checkout {orig}. Error: {error}".format(
372+
orig=orig, error=ex)
373+
)
374+
raise_from(exceptions.CheckoutException(msg), ex)
375+
351376
@reference_exists('remote_branch')
352377
@reference_exists('hash_')
353378
def remote_contains(self, remote_branch, hash_):

integration_tests/test_branch.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ def test_create_branch(repo_root):
149149
assert repo.repo.branches[branch_name].commit.hexsha == tag_0_1_0_hexsha
150150

151151

152+
def test_create_and_checkout_branch(repo_root):
153+
repo = GitRepo(repo_root)
154+
branch_name = "test_create"
155+
156+
assert repo.repo.active_branch.name == 'master'
157+
158+
# Create and check out the new branch
159+
repo.branch.create(branch_name, "0.0.1", checkout=True)
160+
assert repo.repo.active_branch.name == branch_name
161+
162+
repo.repo.heads.master.checkout()
163+
assert repo.repo.active_branch.name == 'master'
164+
165+
# Branch already exists - reset it and don't check it out
166+
repo.branch.create(branch_name, "0.1.0", True, checkout=False)
167+
assert repo.repo.active_branch.name == 'master'
168+
169+
# Branch already exists - reset it and check it out
170+
repo.branch.create(branch_name, "0.0.1", True, checkout=True)
171+
assert repo.repo.active_branch.name == branch_name
172+
173+
152174
def test_remote_contains(repo_root, patch_cleanup, datadir):
153175
repo = GitRepo(repo_root)
154176
remote_branch = "origin/master"

tests/test_branch.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,77 @@ def test_reset_reset_failure(mock_repo):
680680
repo.branch.hard_reset(refresh=False)
681681

682682

683+
def test_reset_to_ref_with_checkout(mock_repo):
684+
"""
685+
GIVEN GitRepo is initialized with a path and repo
686+
WHEN reset is called with checkout
687+
THEN repo.head.reset is called
688+
AND repo.checkout is called once
689+
"""
690+
repo = GitRepo(repo=mock_repo)
691+
with patch('git.repo.fun.name_to_object'):
692+
repo.branch.hard_reset_to_ref("main", "origin/main", checkout=True)
693+
694+
assert mock_repo.head.reset.called is True
695+
assert mock_repo.git.checkout.call_count == 1
696+
697+
698+
def test_reset_to_ref_detached_head_with_checkout(mock_repo, monkeypatch):
699+
"""
700+
GIVEN GitRepo is initialized with a path and repo
701+
WHEN reset is called with checkout
702+
AND the current HEAD is detached
703+
THEN repo.head.reset is called
704+
AND repo.checkout is called once
705+
"""
706+
class MockRef:
707+
@property
708+
def name(self):
709+
# Detached heads don't have a name
710+
raise TypeError
711+
712+
repo = GitRepo(repo=mock_repo)
713+
with patch('git.repo.fun.name_to_object'):
714+
mock_repo.head.ref = MockRef()
715+
repo.branch.hard_reset_to_ref("main", "origin/main", checkout=True)
716+
717+
assert mock_repo.head.reset.called is True
718+
assert mock_repo.git.checkout.call_count == 1
719+
720+
721+
def test_reset_to_ref_without_checkout(mock_repo):
722+
"""
723+
GIVEN GitRepo is initialized with a path and repo
724+
WHEN reset_to_ref is called with checkout False
725+
THEN repo.head.reset is called
726+
AND repo.checkout is called twice to return to the original state
727+
"""
728+
repo = GitRepo(repo=mock_repo)
729+
with patch('git.repo.fun.name_to_object'):
730+
repo.branch.hard_reset_to_ref("main", "origin/main", checkout=False)
731+
732+
assert mock_repo.head.reset.called is True
733+
assert mock_repo.git.checkout.call_count == 2
734+
735+
736+
def test_reset_to_ref_without_checkout_fails(mock_repo):
737+
"""
738+
GIVEN GitRepo is initialized with a path and repo
739+
WHEN reset_to_ref is called with checkout False
740+
AND switching back fails
741+
THEN checkoutException is raised
742+
"""
743+
mock_repo.git.checkout.side_effect = [None, git.GitCommandError('checkout', '')]
744+
745+
repo = GitRepo(repo=mock_repo)
746+
with patch('git.repo.fun.name_to_object'):
747+
with pytest.raises(exceptions.CheckoutException):
748+
repo.branch.hard_reset_to_ref("main", "origin/main", checkout=False)
749+
750+
assert mock_repo.head.reset.called is True
751+
assert mock_repo.git.checkout.call_count == 2
752+
753+
683754
def test_local_branch_exists(mock_repo):
684755
"""
685756
GIVEN GitRepo is initialized with a path and repo
@@ -751,12 +822,29 @@ def test_create_branch(mock_repo):
751822
GIVEN GitRepo is initialized with a path and repo
752823
WHEN branch.create is called with a valid name and start_ref
753824
THEN git.branch is called
825+
AND git.checkout is not called
754826
"""
755827
repo = GitRepo(repo=mock_repo)
756828

757829
with patch('git.repo.fun.name_to_object'):
758830
assert repo.branch.create("test", "123456") is True
759831
repo.git.branch.assert_called_with("test", "123456")
832+
repo.git.checkout.assert_not_called()
833+
834+
835+
def test_create_and_checkout_branch(mock_repo):
836+
"""
837+
GIVEN GitRepo is initialized with a path and repo
838+
WHEN branch.create is called with valid parameters and checkout is True
839+
THEN git.branch is called
840+
AND git.checkout is called
841+
"""
842+
repo = GitRepo(repo=mock_repo)
843+
844+
with patch('git.repo.fun.name_to_object'):
845+
assert repo.branch.create("test", "123456", checkout=True) is True
846+
repo.git.branch.assert_called_with("test", "123456")
847+
repo.git.checkout.assert_called()
760848

761849

762850
def test_create_branch_with_bad_start_ref(mock_repo):
@@ -786,6 +874,24 @@ def test_create_branch_already_exists(mock_repo):
786874
with patch('git.repo.fun.name_to_object'):
787875
repo.branch.create("test", "123456")
788876
assert repo.git.branch.called is False
877+
assert repo.git.checkout.called is False
878+
879+
880+
def test_create_branch_already_exists_and_check_it_out(mock_repo):
881+
"""
882+
GIVEN GitRepo is initialized with a path and repo
883+
WHEN branch.create is called with valid params and checkout is True
884+
AND the branch already exists
885+
THEN git.branch is not called
886+
AND git.checkout is called
887+
"""
888+
repo = GitRepo(repo=mock_repo)
889+
mock_repo.branches = ["test", "master"]
890+
891+
with patch('git.repo.fun.name_to_object'):
892+
repo.branch.create("test", "123456", checkout=True)
893+
assert repo.git.branch.called is False
894+
assert repo.git.checkout.called is True
789895

790896

791897
def test_create_branch_already_exists_and_reset_it(mock_repo):

0 commit comments

Comments
 (0)