diff --git a/docs/options/index.rst b/docs/options/index.rst index 009116c..79516c0 100644 --- a/docs/options/index.rst +++ b/docs/options/index.rst @@ -13,6 +13,7 @@ Config options dirty_template version_file count_commits + prefer_tag version_callback sort_by branch_formatter diff --git a/docs/options/prefer_tag.rst b/docs/options/prefer_tag.rst new file mode 100644 index 0000000..93d4a88 --- /dev/null +++ b/docs/options/prefer_tag.rst @@ -0,0 +1,26 @@ +.. _prefer-tag-option: + +``prefer_tag`` +~~~~~~~~~~~~~~ + +Used together with the :ref:`version-file-option` option. + +By default, when :ref:`version-file-option` is set, any tags in the repo are +ignored (see :issue:`155`). With this option enabled, the latest Git tag takes precedence over the version file content. + +.. note:: + + This option is used only with :ref:`version-file-option`, + and only takes effect when the repo actually contains at least one tag; + otherwise the version file content is used as usual. + +When the tag and the version file content are different, +a warning is logged, so stale version files are easier to notice. + +Type +^^^^ +``bool`` + +Default value +^^^^^^^^^^^^^ +``False`` diff --git a/docs/schemas/file/version_file.rst b/docs/schemas/file/version_file.rst index 1a56a0d..6001424 100644 --- a/docs/schemas/file/version_file.rst +++ b/docs/schemas/file/version_file.rst @@ -66,8 +66,16 @@ of this file (``1.0.0``) will be returned instead default version number. **Please take into account that any tags in the repo are ignored if this option is being used.** +.. note:: + + If you want the latest Git tag to take precedence over the version file + (for example, when a release tag like ``v2.1.0rc1`` is pushed but the + version file still contains ``2.1.0``), set the + :ref:`prefer-tag-option` option to ``true``. + See also """"""""" - :ref:`version-callback` - :ref:`version-file-option` option +- :ref:`prefer-tag-option` option - :ref:`runtime-version` diff --git a/setuptools_git_versioning/defaults.py b/setuptools_git_versioning/defaults.py index 139e401..51c7b4f 100644 --- a/setuptools_git_versioning/defaults.py +++ b/setuptools_git_versioning/defaults.py @@ -13,6 +13,7 @@ "version_callback": None, "version_file": None, "count_commits_from_version_file": False, + "prefer_tag": False, "tag_formatter": None, "branch_formatter": None, "tag_filter": None, diff --git a/setuptools_git_versioning/version.py b/setuptools_git_versioning/version.py index 01efa16..3f32552 100644 --- a/setuptools_git_versioning/version.py +++ b/setuptools_git_versioning/version.py @@ -5,7 +5,7 @@ import os # noqa: TC003 import re from pathlib import Path -from typing import Callable +from typing import Callable, cast # avoid importing 'packaging' because setuptools-git-versioning can be installed using sdist # where 'packaging' is not installed yet @@ -96,6 +96,7 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901 version_callback: str | Callable[[], str] | None = None, version_file: str | os.PathLike | None = None, count_commits_from_version_file: bool = False, + prefer_tag: bool = False, tag_formatter: Callable[[str], str] | str | None = None, branch_formatter: Callable[[str], str] | str | None = None, tag_filter: Callable[[str], str | None] | str | None = None, @@ -134,6 +135,9 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901 raise ValueError(msg) return get_version_from_callback(version_callback, package_name, root=root) + ccount: int | None = 0 + on_tag = False + tag_sha = None head_sha = get_sha(root=root) log.log(INFO, "HEAD SHA-256: %r", head_sha) @@ -146,15 +150,21 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901 tag = get_tag(sort_by=sort_by, root=root, filter_callback=filter_callback) if not tag: log.log(INFO, "No tags found") - tag_sha = None - on_tag = False else: - tag_sha = get_sha(tag, root=root) + tag_sha = cast("str", get_sha(tag, root=root)) log.log(INFO, "Tag SHA-256: %r", tag_sha) on_tag = head_sha is not None and head_sha == tag_sha log.log(INFO, "HEAD is tagged: %r", on_tag) + ccount = count_since(tag_sha, root=root) + log.log(INFO, "Commits count between HEAD and last tag: %r", ccount) + + if tag_formatter is not None: + tag_format_callback = create_tag_formatter(tag_formatter, package_name=package_name, root=root) + tag = tag_format_callback(tag) + log.log(DEBUG, "Tag after formatting: %r", tag) + if version_file: log.log(INFO, "Checking for 'version_file'") @@ -166,35 +176,44 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901 version_file_path, starting_version, ) - tag = None + file_tag = None else: log.log(INFO, "Reading version_file '%s' content", version_file) - tag = version_file_path.read_text().strip() or None + file_tag = version_file_path.read_text().strip() or None - if not tag: + if not file_tag: log.log(INFO, "File %r is empty", version_file) else: - log.log(DEBUG, "File content: %r", tag) - if not count_commits_from_version_file: - return sanitize_version(tag) + log.log(DEBUG, "File content: %r", file_tag) + + if prefer_tag and file_tag and tag: + log.log( + INFO, + "Both 'version_file' and a Git tag (%r) are present, and 'prefer_tag' is enabled: using the tag", + tag, + ) + if file_tag and sanitize_version(file_tag) != sanitize_version(tag): + log.warning( + "Git tag %r and 'version_file' content %r are different: using the tag. " + "Update the version file to silence this warning", + tag, + file_tag, + ) - file_sha = get_latest_file_commit(version_file, root=root) - log.log(DEBUG, "File SHA-256: %r", file_sha) + elif file_tag: + tag = file_tag - ccount = count_since(file_sha, root=root) if file_sha else None - log.log(INFO, "Commits count between HEAD and last version file change: %r", ccount) + if not count_commits_from_version_file: + return sanitize_version(tag) - elif not head_sha: - log.log(INFO, "Not a git repo, or repo without any branch") + file_sha = get_latest_file_commit(version_file, root=root) + log.log(DEBUG, "File SHA-256: %r", file_sha) - elif tag_sha: - ccount = count_since(tag_sha, root=root) - log.log(INFO, "Commits count between HEAD and last tag: %r", ccount) + ccount = count_since(file_sha, root=root) if file_sha else None + log.log(INFO, "Commits count between HEAD and last version file change: %r", ccount) - if tag_formatter is not None: - tag_format_callback = create_tag_formatter(tag_formatter, package_name=package_name, root=root) - tag = tag_format_callback(tag) - log.log(DEBUG, "Tag after formatting: %r", tag) + elif not head_sha: + log.log(INFO, "Not a git repo, or repo without any branch") if not tag: log.log(INFO, "No source for version, return starting_version %r", starting_version) diff --git a/tests/test_integration/test_version_file.py b/tests/test_integration/test_version_file.py index dfac57a..5479c49 100644 --- a/tests/test_integration/test_version_file.py +++ b/tests/test_integration/test_version_file.py @@ -312,3 +312,92 @@ def test_version_file_git_not_executable(repo, create_config, count_commits, tmp tmp_path = tmp_path_factory.mktemp("bin") tmp_path.joinpath("git").touch() assert get_version(repo, env={"PATH": os.fspath(tmp_path)}) == "1.0.0" + + +@pytest.mark.flaky(reruns=3) # sha and full_sha can start with 0 which are removed, just try again +def test_version_file_prefer_tag_off_by_default(repo, create_config): + # without prefer_tag, the version file wins even when a tag is present + create_file(repo, "VERSION.txt", "1.0.0") + create_config( + repo, + { + "version_file": "VERSION.txt", + "count_commits_from_version_file": True, + }, + ) + create_tag(repo, "1.2.3") + + assert get_version(repo) == "1.0.0" + + +@pytest.mark.flaky(reruns=3) # sha and full_sha can start with 0 which are removed, just try again +@pytest.mark.parametrize("count_commits_from_version_file", [True, False]) +def test_version_file_prefer_tag_on_tagged_head(repo, create_config, count_commits_from_version_file): + # prefer_tag makes the tag win over the version file content on a tagged HEAD + create_file(repo, "VERSION.txt", "1.0.0") + create_config( + repo, + { + "version_file": "VERSION.txt", + "count_commits_from_version_file": count_commits_from_version_file, + "prefer_tag": True, + }, + ) + create_tag(repo, "1.2.3") + + assert get_version(repo) == "1.2.3" + + +@pytest.mark.flaky(reruns=3) # sha and full_sha can start with 0 which are removed, just try again +def test_version_file_prefer_tag_dev_commits(repo, create_config): + # prefer_tag + commits after the tag: tag is used as the version source, + # dev template kicks in because HEAD is not on the tag + create_file(repo, "VERSION.txt", "1.0.0") + create_config( + repo, + { + "version_file": "VERSION.txt", + "count_commits_from_version_file": True, + "prefer_tag": True, + }, + ) + create_tag(repo, "1.2.3") + # create_file with default add=True/commit=True creates the commit after the tag + create_file(repo, "work.txt", "work after tag") + + sha = get_sha(repo) + assert get_version(repo) == f"1.2.3.post1+git.{sha}" + + +@pytest.mark.flaky(reruns=3) # sha and full_sha can start with 0 which are removed, just try again +def test_version_file_prefer_tag_without_any_tag(repo, create_config): + # prefer_tag with no tags in the repo: version file is still the source + # (count_commits_from_version_file is off here so the file content is + # returned as-is, without the dev suffix) + create_file(repo, "VERSION.txt", "1.0.0") + create_config( + repo, + { + "version_file": "VERSION.txt", + "prefer_tag": True, + }, + ) + + assert get_version(repo) == "1.0.0" + + +@pytest.mark.flaky(reruns=3) # sha and full_sha can start with 0 which are removed, just try again +def test_version_file_prefer_tag_v_prefix(repo, create_config): + # tag with a "v" prefix is compared to the version file without the prefix + create_file(repo, "VERSION.txt", "1.2.3") + create_config( + repo, + { + "version_file": "VERSION.txt", + "count_commits_from_version_file": True, + "prefer_tag": True, + }, + ) + create_tag(repo, "v1.2.3") + + assert get_version(repo) == "1.2.3"