diff --git a/docs/cli.rst b/docs/cli.rst index c8040461..12baf1d8 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -121,3 +121,10 @@ By default, ``towncrier`` compares the current branch against ``origin/main`` (a Use ``REMOTE-BRANCH`` instead of ``origin/main``:: $ towncrier check --compare-with origin/trunk + +.. option:: --staged + + Include files that have been staged for commit when checking for news fragments:: + + $ towncrier check --staged + $ towncrier check --staged --compare-with origin/trunk diff --git a/src/towncrier/_git.py b/src/towncrier/_git.py index ff3d5448..457e70f4 100644 --- a/src/towncrier/_git.py +++ b/src/towncrier/_git.py @@ -41,7 +41,7 @@ def get_remote_branches(base_directory: str) -> list[str]: def list_changed_files_compared_to_branch( - base_directory: str, compare_with: str + base_directory: str, compare_with: str, include_staged: bool ) -> list[str]: output = check_output( ["git", "diff", "--name-only", compare_with + "..."], @@ -49,5 +49,14 @@ def list_changed_files_compared_to_branch( encoding="utf-8", stderr=STDOUT, ) - - return output.strip().splitlines() + filenames = output.strip().splitlines() + if include_staged: + output = check_output( + ["git", "diff", "--name-only", "--cached"], + cwd=base_directory, + encoding="utf-8", + stderr=STDOUT, + ) + filenames.extend(output.strip().splitlines()) + + return filenames diff --git a/src/towncrier/check.py b/src/towncrier/check.py index 8b057545..bf3c6e2f 100644 --- a/src/towncrier/check.py +++ b/src/towncrier/check.py @@ -57,15 +57,27 @@ def _get_default_compare_branch(branches: Container[str]) -> str | None: metavar="FILE_PATH", help=config_option_help, ) -def _main(compare_with: str | None, directory: str | None, config: str | None) -> None: +@click.option( + "--staged", + "staged", + is_flag=True, + default=False, + help="Include staged files as part of the branch checked in the --compare-with", +) +def _main( + compare_with: str | None, directory: str | None, config: str | None, staged: bool +) -> None: """ Check for new fragments on a branch. """ - __main(compare_with, directory, config) + __main(compare_with, directory, config, staged) def __main( - comparewith: str | None, directory: str | None, config_path: str | None + comparewith: str | None, + directory: str | None, + config_path: str | None, + staged: bool, ) -> None: base_directory, config = load_config_from_options(directory, config_path) @@ -80,7 +92,7 @@ def __main( try: files_changed = list_changed_files_compared_to_branch( - base_directory, comparewith + base_directory, comparewith, staged ) except CalledProcessError as e: click.echo("git produced output while failing:") diff --git a/src/towncrier/newsfragments/676.feature.rst b/src/towncrier/newsfragments/676.feature.rst new file mode 100644 index 00000000..a0e0cfff --- /dev/null +++ b/src/towncrier/newsfragments/676.feature.rst @@ -0,0 +1 @@ +The `towncrier check` command now has a `--staged` flag to inspect the files staged for commit when checking for a news fragment: useful in a pre-commit hook diff --git a/src/towncrier/test/test_check.py b/src/towncrier/test/test_check.py index 9d8c05aa..bfb76191 100644 --- a/src/towncrier/test/test_check.py +++ b/src/towncrier/test/test_check.py @@ -6,7 +6,7 @@ import warnings from pathlib import Path -from subprocess import call +from subprocess import check_call from click.testing import CliRunner from twisted.trial.unittest import TestCase @@ -28,7 +28,7 @@ def create_project( setup_simple_project(pyproject_path=pyproject_path, extra_config=extra_config) Path("foo/newsfragments/123.feature").write_text("Adds levitation") initial_commit(branch=main_branch) - call(["git", "checkout", "-b", "otherbranch"]) + check_call(["git", "checkout", "-b", "otherbranch"]) def commit(message): @@ -37,8 +37,17 @@ def commit(message): There must be uncommitted changes otherwise git will complain: "nothing to commit, working tree clean" """ - call(["git", "add", "."]) - call(["git", "commit", "-m", message]) + check_call(["git", "add", "."]) + check_call(["git", "commit", "-m", message]) + + +def stage(): + """Stage a commit to the repo in the current working directory + + There must be uncommitted changes otherwise git will complain: + "nothing to commit, working tree clean" + """ + check_call(["git", "add", "."]) def initial_commit(branch="main"): @@ -50,11 +59,11 @@ def initial_commit(branch="main"): """ # --initial-branch is explicitly set to `main` because # git has deprecated the default branch name. - call(["git", "init", f"--initial-branch={branch}"]) + check_call(["git", "init", f"--initial-branch={branch}"]) # Without ``git config` user.name and user.email `git commit` fails # unless the settings are set globally - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) + check_call(["git", "config", "user.name", "user"]) + check_call(["git", "config", "user.email", "user@example.com"]) commit("Initial Commit") @@ -156,8 +165,8 @@ def test_fragment_missing(self): with open(file_path, "w") as f: f.write("import os") - call(["git", "add", "foo/somefile.py"]) - call(["git", "commit", "-m", "add a file"]) + check_call(["git", "add", "foo/somefile.py"]) + check_call(["git", "commit", "-m", "add a file"]) result = runner.invoke(towncrier_check, ["--compare-with", "master"]) @@ -204,6 +213,41 @@ def test_fragment_exists_but_not_in_check(self): (result.output, str(fragment_path)), ) + def test_fragment_exists_and_staged(self): + """A fragment exists and is added in staging. Pass only if staging on the command line""" + runner = CliRunner() + + with runner.isolated_filesystem(): + create_project( + "pyproject.toml", + main_branch="master", + extra_config="[[tool.towncrier.type]]\n" + 'directory = "feature"\n' + 'name = "Features"\n' + "showcontent = true\n" + "[[tool.towncrier.type]]\n" + 'directory = "sut"\n' + 'name = "System Under Test"\n' + "showcontent = true\n", + ) + + file_path = "foo/somefile.py" + write(file_path, "import os") + + commit("add some files for test initialization") + + fragment_path = Path("foo/newsfragments/1234.feature").absolute() + write(fragment_path, "Adds gravity back") + stage() + + result = runner.invoke(towncrier_check, ["--compare-with", "master"]) + + self.assertEqual(1, result.exit_code) + result = runner.invoke( + towncrier_check, ["--staged", "--compare-with", "master"] + ) + self.assertEqual(0, result.exit_code) + def test_fragment_exists_and_in_check(self): """ A fragment that exists but is not marked as check=False is @@ -254,8 +298,8 @@ def test_none_stdout_encoding_works(self): with open(fragment_path, "w") as f: f.write("Adds gravity back") - call(["git", "add", fragment_path]) - call(["git", "commit", "-m", "add a newsfragment"]) + check_call(["git", "add", fragment_path]) + check_call(["git", "commit", "-m", "add a newsfragment"]) runner = CliRunner(mix_stderr=False) result = runner.invoke(towncrier_check, ["--compare-with", "master"]) @@ -310,16 +354,18 @@ def test_release_branch(self): commit("First release") # The news file is now created. self.assertIn("NEWS.rst", os.listdir(".")) - call(["git", "checkout", "main"]) - call(["git", "merge", "otherbranch", "-m", "Sync release in main branch."]) + check_call(["git", "checkout", "main"]) + check_call( + ["git", "merge", "otherbranch", "-m", "Sync release in main branch."] + ) # We have a new feature branch that has a news fragment that # will be merged to the main branch. - call(["git", "checkout", "-b", "new-feature-branch"]) + check_call(["git", "checkout", "-b", "new-feature-branch"]) write("foo/newsfragments/456.feature", "Foo the bar") commit("A feature in the second release.") - call(["git", "checkout", "main"]) - call( + check_call(["git", "checkout", "main"]) + check_call( [ "git", "merge", @@ -330,7 +376,7 @@ def test_release_branch(self): ) # We now have the new release branch. - call(["git", "checkout", "-b", "next-release"]) + check_call(["git", "checkout", "-b", "next-release"]) runner.invoke(towncrier_build, ["--yes", "--version", "2.0"]) commit("Second release") @@ -392,7 +438,7 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): (subproject1 / "changelog.d").mkdir(parents=True) (subproject1 / "changelog.d/123.feature").write_text("Adds levitation") initial_commit(branch=main_branch) - call(["git", "checkout", "-b", "otherbranch"]) + check_call(["git", "checkout", "-b", "otherbranch"]) # We add a code change but forget to add a news fragment. write(subproject1 / "foo/somefile.py", "import os")