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..1c74114 100644 --- a/setuptools_git_versioning/version.py +++ b/setuptools_git_versioning/version.py @@ -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, @@ -156,33 +157,63 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901 log.log(INFO, "HEAD is tagged: %r", on_tag) if version_file: - log.log(INFO, "Checking for 'version_file'") - - version_file_path = project_root.joinpath(version_file) - if not version_file_path.exists(): + if prefer_tag and tag is not None: log.log( INFO, - "version_file '%s' does not exist, return starting_version %r", - version_file_path, - starting_version, + "Both 'version_file' and a Git tag (%r) are present, and 'prefer_tag' is enabled: using the tag", + tag, ) - tag = None + if tag_sha is None: + tag_sha = get_sha(tag, root=root) + log.log(DEBUG, "Tag SHA-256: %r", tag_sha) + on_tag = head_sha is not None and tag_sha is not None and head_sha == tag_sha + file_tag = None + version_file_path = project_root.joinpath(version_file) + if version_file_path.exists(): + file_tag = version_file_path.read_text().strip() or None + if file_tag and file_tag.lstrip("v") != tag.lstrip("v"): + 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, + ) + ccount = count_since(tag_sha, root=root) if tag_sha else None + 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) else: - log.log(INFO, "Reading version_file '%s' content", version_file) - tag = version_file_path.read_text().strip() or None - - if not tag: - log.log(INFO, "File %r is empty", version_file) + log.log(INFO, "Checking for 'version_file'") + + version_file_path = project_root.joinpath(version_file) + if not version_file_path.exists(): + log.log( + INFO, + "version_file '%s' does not exist, return starting_version %r", + version_file_path, + starting_version, + ) + tag = None else: - log.log(DEBUG, "File content: %r", tag) - if not count_commits_from_version_file: - return sanitize_version(tag) + log.log(INFO, "Reading version_file '%s' content", version_file) + tag = version_file_path.read_text().strip() or None + + if not 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) - file_sha = get_latest_file_commit(version_file, root=root) - log.log(DEBUG, "File SHA-256: %r", file_sha) + file_sha = get_latest_file_commit(version_file, root=root) + log.log(DEBUG, "File SHA-256: %r", file_sha) - 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) + 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) elif not head_sha: log.log(INFO, "Not a git repo, or repo without any branch") 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"