Skip to content

Commit 673ffbc

Browse files
authored
Fix #78: clear error when a configured branch does not exist (#92)
* Add failing tests for issue #78 (clear error for missing branch) * Fix #78: clear error when a configured branch does not exist - git_checkout detects 'remote branch not found' clone failures and raises a message naming the package, branch and URL, pointing at the mx.ini setting. - git_switch_branch and git_merge_rbranch raise the same clear GitError instead of logging a terse 'No such branch' and calling sys.exit(1), so the update path behaves consistently with checkout. - The worker reports expected WCErrors as a plain error message and keeps the full traceback at debug level only.
1 parent da0b792 commit 673ffbc

6 files changed

Lines changed: 91 additions & 10 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
with a `# managed by mxdev` marker and prunes managed entries that are no longer
1414
configured, while leaving user-defined sources untouched. [jensens]
1515

16+
- Fix #78: Give a clear, actionable error when a configured `branch` does not exist on the
17+
remote (e.g. it was deleted), naming the package, branch and URL and pointing at the
18+
`mx.ini` setting, on both checkout and update. Expected VCS errors are no longer reported
19+
with a full Python traceback (the traceback is kept at debug level). [jensens]
20+
1621

1722
## 5.3.2 (2026-05-30)
1823

src/mxdev/vcs/common.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,14 @@ def worker(working_copies: WorkingCopies, the_queue: queue.Queue) -> None:
426426
return
427427
try:
428428
output = action(**kwargs)
429-
except WCError:
429+
except WCError as e:
430430
with output_lock:
431431
for lvl, msg in wc._output:
432432
lvl(msg)
433-
logger.exception("Can not execute action!")
433+
# WCError is an expected operational failure: show a clean,
434+
# actionable message and keep the full traceback for debug only.
435+
logger.error("%s", e)
436+
logger.debug("Traceback for the error above:", exc_info=True)
434437
working_copies.errors = True
435438
else:
436439
with output_lock:

src/mxdev/vcs/git.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class GitError(common.WCError):
1515
pass
1616

1717

18+
def _branch_not_found_message(name: str, branch: str, url: str) -> str:
19+
"""Build a clear, actionable error for a configured branch that is missing."""
20+
return (
21+
f"Branch '{branch}' for package '{name}' does not exist at '{url}'. "
22+
f"Check the 'branch' setting for [{name}] in your mx.ini."
23+
)
24+
25+
1826
class GitWorkingCopy(common.BaseWorkingCopy):
1927
"""The git working copy.
2028
@@ -117,8 +125,7 @@ def git_merge_rbranch(self, stdout_in: str, stderr_in: str, accept_missing: bool
117125
if accept_missing:
118126
logger.info("No such branch %r", branch)
119127
return (stdout_in, stderr_in)
120-
logger.error("No such branch %r", branch)
121-
sys.exit(1)
128+
raise GitError(_branch_not_found_message(self.source["name"], branch, self.source["url"]))
122129

123130
rbp = self._remote_branch_prefix
124131
cmd = self.run_git(["merge", f"{rbp}/{branch}"], cwd=path)
@@ -151,6 +158,9 @@ def git_checkout(self, **kwargs) -> str | None:
151158
cmd = self.run_git(args)
152159
stdout, stderr = cmd.communicate()
153160
if cmd.returncode != 0:
161+
branch = self.source.get("branch")
162+
if branch and "not found in upstream" in stderr:
163+
raise GitError(_branch_not_found_message(name, branch, url))
154164
raise GitError(f"git cloning of '{name}' failed.\n{stderr}")
155165
if "rev" in self.source:
156166
stdout, stderr = self.git_switch_branch(stdout, stderr)
@@ -206,8 +216,7 @@ def git_switch_branch(self, stdout_in: str, stderr_in: str, accept_missing: bool
206216
self.output((logger.info, f"No such branch {branch}"))
207217
return (stdout_in + stdout, stderr_in + stderr)
208218
else:
209-
self.output((logger.error, f"No such branch {branch}"))
210-
sys.exit(1)
219+
raise GitError(_branch_not_found_message(self.source["name"], branch, self.source["url"]))
211220
# runs the checkout with predetermined arguments
212221
cmd = self.run_git(argv, cwd=path)
213222
stdout, stderr = cmd.communicate()

tests/test_common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,9 @@ def update(self, **kwargs):
525525
common.worker(working_copies, test_queue)
526526

527527
assert working_copies.errors is True
528-
assert "Can not execute action!" in caplog.text
528+
# The actual error message is shown (no generic message / traceback noise).
529+
assert "Test error" in caplog.text
530+
assert "Can not execute action!" not in caplog.text
529531

530532

531533
def test_worker_with_bytes_output(mocker):

tests/test_git.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,56 @@ def test_offline_prevents_vcs_operations(mkgitrepo, src):
304304

305305
# After normal update, should have both files
306306
assert {x for x in path.iterdir()} == {path / ".git", path / "foo", path / "bar"}
307+
308+
309+
def test_checkout_missing_branch_gives_clear_error(mkgitrepo, src, caplog):
310+
"""Cloning a configured branch that does not exist must yield a clear,
311+
actionable error (issue #78) instead of a generic failure + traceback.
312+
"""
313+
import logging
314+
315+
repository = mkgitrepo("repository")
316+
create_default_content(repository)
317+
path = src / "egg"
318+
sources = {
319+
"egg": dict(
320+
vcs="git",
321+
name="egg",
322+
branch="does-not-exist",
323+
url=str(repository.base),
324+
path=str(path),
325+
)
326+
}
327+
328+
with caplog.at_level(logging.ERROR):
329+
vcs_checkout(sources, ["egg"], False)
330+
331+
assert "Branch 'does-not-exist' for package 'egg' does not exist" in caplog.text
332+
assert "Check the 'branch' setting for [egg] in your mx.ini" in caplog.text
333+
# No alarming generic message / traceback for an expected operational error.
334+
assert "Can not execute action!" not in caplog.text
335+
336+
337+
def test_update_missing_branch_gives_clear_error(mkgitrepo, src, caplog):
338+
"""Updating to a configured branch that no longer exists must yield the same
339+
clear error (issue #78) instead of a terse 'No such branch' + sys.exit.
340+
"""
341+
import logging
342+
343+
repository = mkgitrepo("repository")
344+
create_default_content(repository)
345+
path = src / "egg"
346+
347+
sources_ok = {"egg": dict(vcs="git", name="egg", branch="master", url=str(repository.base), path=str(path))}
348+
vcs_checkout(sources_ok, ["egg"], False)
349+
350+
sources_bad = {
351+
"egg": dict(vcs="git", name="egg", branch="does-not-exist", url=str(repository.base), path=str(path))
352+
}
353+
caplog.clear()
354+
with caplog.at_level(logging.ERROR):
355+
vcs_update(sources_bad, ["egg"], False)
356+
357+
assert "Branch 'does-not-exist' for package 'egg' does not exist" in caplog.text
358+
assert "Check the 'branch' setting for [egg] in your mx.ini" in caplog.text
359+
assert "Can not execute action!" not in caplog.text

tests/test_git_additional.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def test_git_merge_rbranch_missing_branch_accept():
275275

276276
def test_git_merge_rbranch_missing_branch_no_accept():
277277
"""Test git_merge_rbranch with missing branch and accept_missing=False."""
278+
from mxdev.vcs.git import GitError
278279
from mxdev.vcs.git import GitWorkingCopy
279280

280281
with patch("mxdev.vcs.common.which", return_value="/usr/bin/git"):
@@ -293,9 +294,12 @@ def test_git_merge_rbranch_missing_branch_no_accept():
293294
mock_process.communicate.return_value = ("* main\n develop\n", "")
294295

295296
with patch.object(wc, "run_git", return_value=mock_process):
296-
with pytest.raises(SystemExit):
297+
with pytest.raises(GitError) as excinfo:
297298
wc.git_merge_rbranch("", "", accept_missing=False)
298299

300+
assert "Branch 'nonexistent' for package 'test-package' does not exist" in str(excinfo.value)
301+
assert "Check the 'branch' setting for [test-package] in your mx.ini" in str(excinfo.value)
302+
299303

300304
def test_git_merge_rbranch_merge_failure():
301305
"""Test git_merge_rbranch handles merge failure."""
@@ -888,6 +892,7 @@ def test_git_switch_branch_failure():
888892

889893
def test_git_switch_branch_missing_no_accept():
890894
"""Test git_switch_branch with missing branch and accept_missing=False."""
895+
from mxdev.vcs.git import GitError
891896
from mxdev.vcs.git import GitWorkingCopy
892897

893898
with patch("mxdev.vcs.common.which", return_value="/usr/bin/git"):
@@ -905,8 +910,12 @@ def test_git_switch_branch_missing_no_accept():
905910
mock_process.communicate.return_value = ("* main\n", "")
906911

907912
with patch.object(wc, "run_git", return_value=mock_process):
908-
with pytest.raises(SystemExit):
909-
wc.git_switch_branch("", "", accept_missing=False)
913+
with patch.object(wc, "git_version", return_value=(2, 30, 0)):
914+
with pytest.raises(GitError) as excinfo:
915+
wc.git_switch_branch("", "", accept_missing=False)
916+
917+
assert "Branch 'nonexistent' for package 'test-package' does not exist" in str(excinfo.value)
918+
assert "Check the 'branch' setting for [test-package] in your mx.ini" in str(excinfo.value)
910919

911920

912921
def test_git_switch_branch_missing_accept():

0 commit comments

Comments
 (0)