Skip to content

Commit 63af5aa

Browse files
[fix] Automate branch selection for porting changelog #646
Implemented automated branch detection to avoid choosing between master and main when only one exists. Fixes #646
1 parent fe1cc60 commit 63af5aa

3 files changed

Lines changed: 125 additions & 8 deletions

File tree

openwisp_utils/releaser/release.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from openwisp_utils.releaser.utils import (
2121
SkipSignal,
2222
adjust_markdown_headings,
23+
branch_exists,
2324
demote_markdown_headings,
2425
format_file_with_docstrfmt,
2526
get_current_branch,
@@ -176,10 +177,23 @@ def port_changelog_to_main(gh, config, version, changelog_body, original_branch)
176177
full_block_to_port = f"{version_header}\n{underline}\n\n{changelog_body}"
177178

178179
try:
179-
main_branch = questionary.select(
180-
"Which branch should the changelog be ported to?",
181-
choices=MAIN_BRANCHES,
182-
).ask()
180+
main_exists = branch_exists("main")
181+
master_exists = branch_exists("master")
182+
if main_exists and master_exists:
183+
main_branch = questionary.select(
184+
"Which branch should the changelog be ported to?",
185+
choices=MAIN_BRANCHES,
186+
).ask()
187+
elif main_exists:
188+
main_branch = "main"
189+
elif master_exists:
190+
main_branch = "master"
191+
else:
192+
print(
193+
"Neither 'master' nor 'main' branch was found locally. "
194+
"Skipping changelog porting."
195+
)
196+
return
183197

184198
if not main_branch:
185199
print("Porting cancelled.")

openwisp_utils/releaser/tests/test_release.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,26 +155,112 @@ def test_main_flow_pr_merge_wait(mock_all):
155155
@patch("openwisp_utils.releaser.release.update_changelog_file")
156156
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
157157
@patch("openwisp_utils.releaser.release.subprocess.run")
158+
@patch("openwisp_utils.releaser.release.branch_exists")
158159
@patch("openwisp_utils.releaser.release.questionary")
159160
def test_port_changelog_to_main_flow(
160-
mock_questionary, mock_subprocess, mock_format_file, mock_update_changelog
161+
mock_questionary,
162+
mock_branch_exists,
163+
mock_subprocess,
164+
mock_format_file,
165+
mock_update_changelog,
161166
):
162167
"""Tests the changelog porting process for both RST and MD files, and the cancellation path."""
163168
mock_gh = MagicMock()
164169
mock_config_rst = {"changelog_path": "CHANGES.rst"}
170+
# Both branches exist: user is asked
171+
mock_branch_exists.return_value = True
165172
mock_questionary.select.return_value.ask.return_value = "main"
166173
port_changelog_to_main(mock_gh, mock_config_rst, "1.1.1", "- fix", "1.1.x")
167174
mock_gh.create_pr.assert_called_once()
168175
mock_format_file.assert_called_once_with("CHANGES.rst")
169176

170177
mock_gh.reset_mock()
171178

172-
# Test Cancellation path
179+
# Test Cancellation path (when both branches exist and user cancels)
173180
mock_questionary.select.return_value.ask.return_value = None
174181
port_changelog_to_main(mock_gh, mock_config_rst, "1.1.1", "- fix", "1.1.x")
175182
mock_gh.create_pr.assert_not_called()
176183

177184

185+
@patch("openwisp_utils.releaser.release.update_changelog_file")
186+
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
187+
@patch("openwisp_utils.releaser.release.subprocess.run")
188+
@patch("openwisp_utils.releaser.release.branch_exists")
189+
def test_port_changelog_only_master_exists(
190+
mock_branch_exists, mock_subprocess, mock_format_file, mock_update_changelog
191+
):
192+
"""`master` is auto-selected when `main` does not exist locally."""
193+
mock_gh = MagicMock()
194+
mock_config = {"changelog_path": "CHANGES.rst"}
195+
# Simulate: main=False, master=True
196+
mock_branch_exists.side_effect = lambda name: name == "master"
197+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
198+
mock_gh.create_pr.assert_called_once()
199+
# Verify PR was opened against master
200+
assert mock_gh.create_pr.call_args[0][1] == "master"
201+
202+
203+
@patch("openwisp_utils.releaser.release.update_changelog_file")
204+
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
205+
@patch("openwisp_utils.releaser.release.subprocess.run")
206+
@patch("openwisp_utils.releaser.release.branch_exists")
207+
def test_port_changelog_only_main_exists(
208+
mock_branch_exists, mock_subprocess, mock_format_file, mock_update_changelog
209+
):
210+
"""`main` is auto-selected when it exists and `master` does not."""
211+
mock_gh = MagicMock()
212+
mock_config = {"changelog_path": "CHANGES.rst"}
213+
# Simulate: main=True, master=False
214+
mock_branch_exists.side_effect = lambda name: name == "main"
215+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
216+
mock_gh.create_pr.assert_called_once()
217+
# Verify PR was opened against main
218+
assert mock_gh.create_pr.call_args[0][1] == "main"
219+
220+
221+
@patch("openwisp_utils.releaser.release.update_changelog_file")
222+
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
223+
@patch("openwisp_utils.releaser.release.subprocess.run")
224+
@patch("openwisp_utils.releaser.release.branch_exists")
225+
@patch("openwisp_utils.releaser.release.questionary")
226+
def test_port_changelog_both_branches_prompts_user(
227+
mock_questionary,
228+
mock_branch_exists,
229+
mock_subprocess,
230+
mock_format_file,
231+
mock_update_changelog,
232+
):
233+
"""User is prompted to choose when both `main` and `master` exist."""
234+
mock_gh = MagicMock()
235+
mock_config = {"changelog_path": "CHANGES.rst"}
236+
# Both branches exist
237+
mock_branch_exists.return_value = True
238+
mock_questionary.select.return_value.ask.return_value = "master"
239+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
240+
mock_questionary.select.assert_called_once()
241+
mock_gh.create_pr.assert_called_once()
242+
assert mock_gh.create_pr.call_args[0][1] == "master"
243+
244+
245+
@patch("openwisp_utils.releaser.release.update_changelog_file")
246+
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
247+
@patch("openwisp_utils.releaser.release.subprocess.run")
248+
@patch("openwisp_utils.releaser.release.branch_exists")
249+
def test_port_changelog_neither_branch_exists(
250+
mock_branch_exists, mock_subprocess, mock_format_file, mock_update_changelog
251+
):
252+
"""Porting is skipped with a message if neither branch exists."""
253+
mock_gh = MagicMock()
254+
mock_config = {"changelog_path": "CHANGES.rst"}
255+
# Neither exists
256+
mock_branch_exists.return_value = False
257+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
258+
# Verify no PR was created
259+
mock_gh.create_pr.assert_not_called()
260+
# Verify no file update was attempted
261+
mock_update_changelog.assert_not_called()
262+
263+
178264
def test_main_bugfix_flow_with_porting(mock_all, mocker):
179265
"""Tests the main release flow for a bugfix, including accepting the changelog port."""
180266
mock_all["_git_command_map"][
@@ -239,11 +325,15 @@ def test_main_flow_skip_release_creation(mock_all):
239325
)
240326

241327

328+
@patch("openwisp_utils.releaser.release.branch_exists")
242329
@patch("openwisp_utils.releaser.release.subprocess.run")
243-
def test_port_changelog_to_main_flow_markdown(mock_subprocess, mock_all):
330+
def test_port_changelog_to_main_flow_markdown(
331+
mock_subprocess, mock_branch_exists, mock_all
332+
):
244333
"""Tests the changelog porting process for a Markdown file."""
245334
mock_gh = MagicMock()
246335
mock_config_md = {"changelog_path": "CHANGES.md"}
336+
mock_branch_exists.return_value = True
247337
mock_all["questionary_select"].return_value.ask.return_value = "main"
248338

249339
with patch("openwisp_utils.releaser.release.update_changelog_file") as mock_update:
@@ -253,12 +343,14 @@ def test_port_changelog_to_main_flow_markdown(mock_subprocess, mock_all):
253343
assert "## Version 1.1.1" in called_with_content
254344

255345

346+
@patch("openwisp_utils.releaser.release.branch_exists")
256347
@patch("openwisp_utils.releaser.release.subprocess.run")
257-
def test_port_changelog_skip_pr_creation(mock_subprocess, mock_all):
348+
def test_port_changelog_skip_pr_creation(mock_subprocess, mock_branch_exists, mock_all):
258349
"""Tests skipping PR creation during changelog porting."""
259350
mock_gh = MagicMock()
260351
mock_gh.create_pr.side_effect = SkipSignal
261352
mock_config = {"changelog_path": "CHANGES.rst"}
353+
mock_branch_exists.return_value = True
262354
mock_all["questionary_select"].return_value.ask.return_value = "main"
263355

264356
with patch("openwisp_utils.releaser.release.update_changelog_file"):

openwisp_utils/releaser/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ def get_current_branch():
8585
return result.stdout.strip()
8686

8787

88+
def branch_exists(branch_name):
89+
"""Check if a Git branch exists locally."""
90+
result = subprocess.run(
91+
["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"],
92+
capture_output=True,
93+
text=True,
94+
encoding="utf-8",
95+
)
96+
return result.returncode == 0
97+
98+
8899
def rst_to_markdown(text):
89100
"""Convert reStructuredText to Markdown using pypandoc."""
90101
escaped_text = re.sub(r"(?<!`)_", r"\\_", text)

0 commit comments

Comments
 (0)