diff --git a/src/towncrier/check.py b/src/towncrier/check.py index 67f8ed72..50510385 100644 --- a/src/towncrier/check.py +++ b/src/towncrier/check.py @@ -6,6 +6,7 @@ import sys from subprocess import STDOUT, CalledProcessError, check_output +from warnings import warn import click @@ -18,10 +19,28 @@ def _run(args, **kwargs): return check_output(args, **kwargs) +def get_default_compare_branch(base_directory, encoding): + branches = ( + _run(["git", "branch", "-r"], cwd=base_directory).decode(encoding).splitlines() + ) + branches = [branch.strip() for branch in branches] + if "origin/main" in branches: + return "origin/main" + if "origin/master" in branches: + warn( + 'Using "origin/master" as default compare branch is deprecated ' + "and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) + return "origin/master" + return None + + @click.command(name="check") @click.option( "--compare-with", - default="origin/master", + default=None, metavar="BRANCH", help=( "Checks files changed running git diff --name-ony BRANCH... " @@ -58,6 +77,14 @@ def __main(comparewith, directory, config): # when the attribute is present but set to None (explicitly piped output # and also some CI such as GitHub Actions). encoding = getattr(sys.stdout, "encoding", "utf8") + if comparewith is None: + comparewith = get_default_compare_branch( + base_directory=base_directory, encoding=encoding + ) + + if comparewith is None: + click.echo("Could not detect default branch. Aborting.") + sys.exit(1) try: files_changed = ( diff --git a/src/towncrier/newsfragments/400.removal.rst b/src/towncrier/newsfragments/400.removal.rst new file mode 100644 index 00000000..783d7d59 --- /dev/null +++ b/src/towncrier/newsfragments/400.removal.rst @@ -0,0 +1,2 @@ +Default branch for `towncrier check` is now "origin/main" instead of "origin/master". +If "origin/main" does not exist, fallback to "origin/master" with deprecation warning. diff --git a/src/towncrier/test/test_check.py b/src/towncrier/test/test_check.py index fabc3e7c..f918318a 100644 --- a/src/towncrier/test/test_check.py +++ b/src/towncrier/test/test_check.py @@ -6,10 +6,12 @@ import sys from subprocess import PIPE, Popen, call +from unittest.mock import patch from click.testing import CliRunner from twisted.trial.unittest import TestCase +from towncrier import check from towncrier.check import _main as towncrier_check @@ -271,3 +273,47 @@ def test_release_branch(self): # Assert self.assertEqual(0, result.exit_code, (result, result.output)) self.assertIn("Checks SKIPPED: news file changes detected", result.output) + + def test_get_default_compare_branch_missingf(self): + """ + If there's no recognized remote origin, exit with an error. + """ + runner = CliRunner() + + with runner.isolated_filesystem(): + create_project() + + result = runner.invoke(towncrier_check) + + self.assertEqual(1, result.exit_code) + self.assertEqual("Could not detect default branch. Aborting.\n", result.output) + + def test_get_default_compare_branch_main(self): + """ + If there's a remote branch origin/main, prefer it over everything else. + """ + runner = CliRunner() + + with runner.isolated_filesystem(): + create_project() + + with patch("towncrier.check._run") as m: + m.return_value = b" origin/master\n origin/main\n\n" + branch = check.get_default_compare_branch(".", "utf-8") + + self.assertEqual("origin/main", branch) + + def test_get_default_compare_branch_fallback(self): + """ + If there's origin/master and no main, use it. + """ + runner = CliRunner() + + with runner.isolated_filesystem(): + create_project() + + with patch("towncrier.check._run") as m: + m.return_value = b" origin/master\n origin/foo\n\n" + branch = check.get_default_compare_branch(".", "utf-8") + + self.assertEqual("origin/master", branch)