Skip to content

Commit 852ffec

Browse files
authored
Ensure that cherry-picker doesn't exit with bad state on branch cleanup (#61)
* Check out previous branch rather than default branch on cleanup * Add tests
1 parent ef217aa commit 852ffec

File tree

2 files changed

+100
-17
lines changed

2 files changed

+100
-17
lines changed

cherry_picker/cherry_picker.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
CHECKING_OUT_DEFAULT_BRANCH
3939
CHECKED_OUT_DEFAULT_BRANCH
4040
41+
CHECKING_OUT_PREVIOUS_BRANCH
42+
CHECKED_OUT_PREVIOUS_BRANCH
43+
4144
PUSHING_TO_REMOTE
4245
PUSHED_TO_REMOTE
4346
PUSHING_TO_REMOTE_FAILED
@@ -138,6 +141,11 @@ def set_paused_state(self):
138141
save_cfg_vals_to_git_cfg(config_path=self.chosen_config_path)
139142
set_state(WORKFLOW_STATES.BACKPORT_PAUSED)
140143

144+
def remember_previous_branch(self):
145+
"""Save the current branch into Git config to be able to get back to it later."""
146+
current_branch = get_current_branch()
147+
save_cfg_vals_to_git_cfg(previous_branch=current_branch)
148+
141149
@property
142150
def upstream(self):
143151
"""Get the remote name to use for upstream branches
@@ -184,24 +192,29 @@ def run_cmd(self, cmd):
184192
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
185193
return output.decode("utf-8")
186194

187-
def checkout_branch(self, branch_name):
188-
"""git checkout -b <branch_name>"""
189-
cmd = [
190-
"git",
191-
"checkout",
192-
"-b",
193-
self.get_cherry_pick_branch(branch_name),
194-
f"{self.upstream}/{branch_name}",
195-
]
195+
def checkout_branch(self, branch_name, *, create_branch=False):
196+
"""git checkout [-b] <branch_name>"""
197+
if create_branch:
198+
checked_out_branch = self.get_cherry_pick_branch(branch_name)
199+
cmd = [
200+
"git",
201+
"checkout",
202+
"-b",
203+
checked_out_branch,
204+
f"{self.upstream}/{branch_name}",
205+
]
206+
else:
207+
checked_out_branch = branch_name
208+
cmd = ["git", "checkout", branch_name]
196209
try:
197210
self.run_cmd(cmd)
198211
except subprocess.CalledProcessError as err:
199212
click.echo(
200-
f"Error checking out the branch {self.get_cherry_pick_branch(branch_name)}."
213+
f"Error checking out the branch {branch_name}."
201214
)
202215
click.echo(err.output)
203216
raise BranchCheckoutException(
204-
f"Error checking out the branch {self.get_cherry_pick_branch(branch_name)}."
217+
f"Error checking out the branch {branch_name}."
205218
)
206219

207220
def get_commit_message(self, commit_sha):
@@ -225,11 +238,23 @@ def checkout_default_branch(self):
225238
"""git checkout default branch"""
226239
set_state(WORKFLOW_STATES.CHECKING_OUT_DEFAULT_BRANCH)
227240

228-
cmd = "git", "checkout", self.config["default_branch"]
229-
self.run_cmd(cmd)
241+
self.checkout_branch(self.config["default_branch"])
230242

231243
set_state(WORKFLOW_STATES.CHECKED_OUT_DEFAULT_BRANCH)
232244

245+
def checkout_previous_branch(self):
246+
"""git checkout previous branch"""
247+
set_state(WORKFLOW_STATES.CHECKING_OUT_PREVIOUS_BRANCH)
248+
249+
previous_branch = load_val_from_git_cfg("previous_branch")
250+
if previous_branch is None:
251+
self.checkout_default_branch()
252+
return
253+
254+
self.checkout_branch(previous_branch)
255+
256+
set_state(WORKFLOW_STATES.CHECKED_OUT_PREVIOUS_BRANCH)
257+
233258
def status(self):
234259
"""
235260
git status
@@ -363,7 +388,12 @@ def cleanup_branch(self, branch):
363388
Switch to the default branch before that.
364389
"""
365390
set_state(WORKFLOW_STATES.REMOVING_BACKPORT_BRANCH)
366-
self.checkout_default_branch()
391+
try:
392+
self.checkout_previous_branch()
393+
except BranchCheckoutException:
394+
click.echo(f"branch {branch} NOT deleted.")
395+
set_state(WORKFLOW_STATES.REMOVING_BACKPORT_BRANCH_FAILED)
396+
return
367397
try:
368398
self.delete_branch(branch)
369399
except subprocess.CalledProcessError:
@@ -378,14 +408,15 @@ def backport(self):
378408
raise click.UsageError("At least one branch must be specified.")
379409
set_state(WORKFLOW_STATES.BACKPORT_STARTING)
380410
self.fetch_upstream()
411+
self.remember_previous_branch()
381412

382413
set_state(WORKFLOW_STATES.BACKPORT_LOOPING)
383414
for maint_branch in self.sorted_branches:
384415
set_state(WORKFLOW_STATES.BACKPORT_LOOP_START)
385416
click.echo(f"Now backporting '{self.commit_sha1}' into '{maint_branch}'")
386417

387418
cherry_pick_branch = self.get_cherry_pick_branch(maint_branch)
388-
self.checkout_branch(maint_branch)
419+
self.checkout_branch(maint_branch, create_branch=True)
389420
commit_message = ""
390421
try:
391422
self.cherry_pick()
@@ -419,6 +450,7 @@ def backport(self):
419450
self.set_paused_state()
420451
return # to preserve the correct state
421452
set_state(WORKFLOW_STATES.BACKPORT_LOOP_END)
453+
reset_stored_previous_branch()
422454
reset_state()
423455

424456
def abort_cherry_pick(self):
@@ -440,6 +472,7 @@ def abort_cherry_pick(self):
440472
if get_current_branch().startswith("backport-"):
441473
self.cleanup_branch(get_current_branch())
442474

475+
reset_stored_previous_branch()
443476
reset_stored_config_ref()
444477
reset_state()
445478

@@ -499,6 +532,7 @@ def continue_cherry_pick(self):
499532
)
500533
set_state(WORKFLOW_STATES.CONTINUATION_FAILED)
501534

535+
reset_stored_previous_branch()
502536
reset_stored_config_ref()
503537
reset_state()
504538

@@ -828,6 +862,11 @@ def reset_stored_config_ref():
828862
"""Config file pointer is not stored in Git config."""
829863

830864

865+
def reset_stored_previous_branch():
866+
"""Remove the previous branch information from Git config."""
867+
wipe_cfg_vals_from_git_cfg("previous_branch")
868+
869+
831870
def reset_state():
832871
"""Remove the progress state from Git config."""
833872
wipe_cfg_vals_from_git_cfg("state")

cherry_picker/test_cherry_picker.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ def git_commit():
8484
)
8585

8686

87+
@pytest.fixture
88+
def git_worktree():
89+
git_worktree_cmd = "git", "worktree"
90+
return lambda *extra_args: (
91+
subprocess.run(git_worktree_cmd + extra_args, check=True)
92+
)
93+
94+
8795
@pytest.fixture
8896
def git_cherry_pick():
8997
git_cherry_pick_cmd = "git", "cherry-pick"
@@ -100,12 +108,13 @@ def git_config():
100108

101109
@pytest.fixture
102110
def tmp_git_repo_dir(tmpdir, cd, git_init, git_commit, git_config):
103-
cd(tmpdir)
111+
repo_dir = tmpdir.mkdir("tmp-git-repo")
112+
cd(repo_dir)
104113
git_init()
105114
git_config("--local", "user.name", "Monty Python")
106115
git_config("--local", "user.email", "[email protected]")
107116
git_commit("Initial commit", "--allow-empty")
108-
yield tmpdir
117+
yield repo_dir
109118

110119

111120
@mock.patch("subprocess.check_output")
@@ -545,13 +554,19 @@ def test_paused_flow(tmp_git_repo_dir, git_add, git_commit):
545554
WORKFLOW_STATES.CHECKING_OUT_DEFAULT_BRANCH,
546555
WORKFLOW_STATES.CHECKED_OUT_DEFAULT_BRANCH,
547556
),
557+
(
558+
"checkout_previous_branch",
559+
WORKFLOW_STATES.CHECKING_OUT_PREVIOUS_BRANCH,
560+
WORKFLOW_STATES.CHECKED_OUT_PREVIOUS_BRANCH,
561+
),
548562
),
549563
)
550564
def test_start_end_states(method_name, start_state, end_state, tmp_git_repo_dir):
551565
assert get_state() == WORKFLOW_STATES.UNSET
552566

553567
with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True):
554568
cherry_picker = CherryPicker("origin", "xxx", [])
569+
cherry_picker.remember_previous_branch()
555570
assert get_state() == WORKFLOW_STATES.UNSET
556571

557572
def _fetch(cmd):
@@ -572,6 +587,22 @@ def test_cleanup_branch(tmp_git_repo_dir, git_checkout):
572587
git_checkout("-b", "some_branch")
573588
cherry_picker.cleanup_branch("some_branch")
574589
assert get_state() == WORKFLOW_STATES.REMOVED_BACKPORT_BRANCH
590+
assert get_current_branch() == "main"
591+
592+
593+
def test_cleanup_branch_checkout_previous_branch(tmp_git_repo_dir, git_checkout, git_worktree):
594+
assert get_state() == WORKFLOW_STATES.UNSET
595+
596+
with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True):
597+
cherry_picker = CherryPicker("origin", "xxx", [])
598+
assert get_state() == WORKFLOW_STATES.UNSET
599+
600+
git_checkout("-b", "previous_branch")
601+
cherry_picker.remember_previous_branch()
602+
git_checkout("-b", "some_branch")
603+
cherry_picker.cleanup_branch("some_branch")
604+
assert get_state() == WORKFLOW_STATES.REMOVED_BACKPORT_BRANCH
605+
assert get_current_branch() == "previous_branch"
575606

576607

577608
def test_cleanup_branch_fail(tmp_git_repo_dir):
@@ -585,6 +616,19 @@ def test_cleanup_branch_fail(tmp_git_repo_dir):
585616
assert get_state() == WORKFLOW_STATES.REMOVING_BACKPORT_BRANCH_FAILED
586617

587618

619+
def test_cleanup_branch_checkout_fail(tmp_git_repo_dir, tmpdir, git_checkout, git_worktree):
620+
assert get_state() == WORKFLOW_STATES.UNSET
621+
622+
with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True):
623+
cherry_picker = CherryPicker("origin", "xxx", [])
624+
assert get_state() == WORKFLOW_STATES.UNSET
625+
626+
git_checkout("-b", "some_branch")
627+
git_worktree("add", str(tmpdir.mkdir("test-worktree")), "main")
628+
cherry_picker.cleanup_branch("some_branch")
629+
assert get_state() == WORKFLOW_STATES.REMOVING_BACKPORT_BRANCH_FAILED
630+
631+
588632
def test_cherry_pick(tmp_git_repo_dir, git_add, git_branch, git_commit, git_checkout):
589633
cherry_pick_target_branches = ("3.8",)
590634
pr_remote = "origin"

0 commit comments

Comments
 (0)