diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3e7023a34..9db18bf55 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,6 +13,9 @@ jobs: steps: - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: diff --git a/semantic_release/cli.py b/semantic_release/cli.py index bed36b18a..b5d17b558 100644 --- a/semantic_release/cli.py +++ b/semantic_release/cli.py @@ -16,8 +16,10 @@ from .history import ( evaluate_version_bump, get_current_version, + get_current_release_version, get_new_version, get_previous_version, + get_previous_release_version, set_new_version, ) from .history.logs import generate_changelog @@ -98,7 +100,9 @@ def print_version(*, current=False, force_level=None, prerelease=False, **kwargs Print the current or new version to standard output. """ try: - current_version = get_current_version(prerelease) + current_version = get_current_version() + current_release_version = get_current_release_version() + logger.info(f"Current version: {current_version}, Current release version: {current_release_version}") except GitError as e: print(str(e), file=sys.stderr) return False @@ -108,7 +112,7 @@ def print_version(*, current=False, force_level=None, prerelease=False, **kwargs # Find what the new version number should be level_bump = evaluate_version_bump(current_version, force_level) - new_version = get_new_version(current_version, level_bump, prerelease) + new_version = get_new_version(current_version, current_release_version, level_bump, prerelease) if should_bump_version(current_version=current_version, new_version=new_version): print(new_version, end="") return True @@ -130,14 +134,15 @@ def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kw # Get the current version number try: - current_version = get_current_version(prerelease) - logger.info(f"Current version: {current_version}") + current_version = get_current_version() + current_release_version = get_current_release_version() + logger.info(f"Current version: {current_version}, Current release version: {current_release_version}") except GitError as e: logger.error(str(e)) return False # Find what the new version number should be - level_bump = evaluate_version_bump(current_version, force_level) - new_version = get_new_version(current_version, level_bump, prerelease) + level_bump = evaluate_version_bump(current_release_version, force_level) + new_version = get_new_version(current_version, current_release_version, level_bump, prerelease) if not should_bump_version( current_version=current_version, new_version=new_version, retry=retry, noop=noop @@ -207,7 +212,7 @@ def changelog(*, unreleased=False, noop=False, post=False, prerelease=False, **k :raises ImproperConfigurationError: if there is no current version """ - current_version = get_current_version(prerelease) + current_version = get_current_version() if current_version is None: raise ImproperConfigurationError( "Unable to get the current version. " @@ -245,7 +250,9 @@ def publish( retry: bool = False, noop: bool = False, prerelease: bool = False, **kwargs ): """Run the version task, then push to git and upload to an artifact repository / GitHub Releases.""" - current_version = get_current_version(prerelease) + current_version = get_current_version() + current_release_version = get_current_release_version() + logger.info(f"Current version: {current_version}, Current release version: {current_release_version}") verbose = logger.isEnabledFor(logging.DEBUG) if retry: @@ -254,11 +261,11 @@ def publish( # "current" version will be the previous version. level_bump = None new_version = current_version - current_version = get_previous_version(current_version) + current_version = get_previous_release_version(current_version) else: # Calculate the new version level_bump = evaluate_version_bump(current_version, kwargs.get("force_level")) - new_version = get_new_version(current_version, level_bump, prerelease) + new_version = get_new_version(current_version, current_release_version, level_bump, prerelease) owner, name = get_repository_owner_and_name() diff --git a/semantic_release/history/__init__.py b/semantic_release/history/__init__.py index fa9e6f67c..7e0d617f0 100644 --- a/semantic_release/history/__init__.py +++ b/semantic_release/history/__init__.py @@ -25,6 +25,14 @@ logger = logging.getLogger(__name__) +prerelease_pattern = f"-{config.get('prerelease_tag')}\.\d+" +version_pattern = f"(\d+\.\d+\.\d+({prerelease_pattern})?)" +release_version_pattern = f"(\d+\.\d+\.\d+(?!{prerelease_pattern}))" + +release_version_regex = rf"{release_version_pattern}" +version_regex = rf"{version_pattern}" + + class VersionDeclaration(ABC): def __init__(self, path: Union[str, Path]): self.path = Path(path) @@ -46,7 +54,7 @@ def from_variable(config_str: str): """ path, variable = config_str.split(":", 1) pattern = ( - rf'{variable} *[:=] *["\']{PatternVersionDeclaration.version_regex}["\']' + rf'{variable} *[:=] *["\']{version_regex}["\']' ) return PatternVersionDeclaration(path, pattern) @@ -57,7 +65,7 @@ def from_pattern(config_str: str): regular expression matching the version number. """ path, pattern = config_str.split(":", 1) - pattern = pattern.format(version=PatternVersionDeclaration.version_regex) + pattern = pattern.format(version=version_regex) return PatternVersionDeclaration(path, pattern) @abstractmethod @@ -116,8 +124,6 @@ class PatternVersionDeclaration(VersionDeclaration): function to create the version patterns specified in the config files. """ - version_regex = r"(\d+\.\d+(?:\.\d+)?)" - # The pattern should be a regular expression with a single group, # containing the version to replace. def __init__(self, path: str, pattern: str): @@ -176,18 +182,29 @@ def swap_version(m): self.path.write_text(new_content) -def get_prerelease_pattern() -> str: - return "-" + config.get("prerelease_tag") + "." +@LoggedFunction(logger) +def get_current_version_by_tag() -> str: + """ + Find the current version of the package in the current working directory using git tags. + + :return: A string with the version number or 0.0.0 on failure. + """ + version = get_last_version(pattern=version_pattern) + if version: + return version + + logger.debug("no version found, returning default of v0.0.0") + return "0.0.0" @LoggedFunction(logger) -def get_current_version_by_tag(omit_pattern=None) -> str: +def get_current_release_version_by_tag() -> str: """ Find the current version of the package in the current working directory using git tags. :return: A string with the version number or 0.0.0 on failure. """ - version = get_last_version(omit_pattern=omit_pattern) + version = get_last_version(pattern=release_version_pattern) if version: return version @@ -196,7 +213,7 @@ def get_current_version_by_tag(omit_pattern=None) -> str: @LoggedFunction(logger) -def get_current_version_by_config_file(omit_pattern=None) -> str: +def get_current_version_by_config_file() -> str: """ Get current version from the version variable defined in the configuration. @@ -220,25 +237,36 @@ def get_current_version_by_config_file(omit_pattern=None) -> str: return version -def get_current_version(prerelease_version: bool = False) -> str: +def get_current_version() -> str: """ Get current version from tag or version variable, depending on configuration. - This will not return prerelease versions. + This can be either a release or prerelease version :return: A string with the current version number """ - omit_pattern = None if prerelease_version else get_prerelease_pattern() if config.get("version_source") in ["tag", "tag_only"]: - return get_current_version_by_tag(omit_pattern) - current_version = get_current_version_by_config_file(omit_pattern) - if omit_pattern and omit_pattern in current_version: - return get_previous_version(current_version) - return current_version + return get_current_version_by_tag() + else: + return get_current_version_by_config_file() + + +def get_current_release_version() -> str: + """ + Get current release version from tag or commit message (no going back in config file), + depending on configuration. + This will return the current release version (NOT prerelease), instead of just the current version + + :return: A string with the current version number + """ + if config.get("version_source") == "tag": + return get_current_release_version_by_tag() + else: + return get_current_release_version_by_commits() @LoggedFunction(logger) def get_new_version( - current_version: str, level_bump: str, prerelease: bool = False + current_version: str, current_release_version: str, level_bump: str, prerelease: bool = False ) -> str: """ Calculate the next version based on the given bump level with semver. @@ -249,34 +277,46 @@ def get_new_version( :param prerelease: Should the version bump be marked as a prerelease :return: A string with the next version number. """ - if not level_bump: - logger.debug("No bump requested, using input version") - new_version = current_version + # pre or release version + current_version_info = semver.VersionInfo.parse(current_version) + # release version + current_release_version_info = semver.VersionInfo.parse(current_release_version) + + # sanity check + # if the current version is no prerelease, than + # current_version and current_release_version must be the same + if not current_version_info.prerelease and current_version_info.compare(current_release_version_info) != 0: + raise ValueError() + + if level_bump: + next_version_info = current_release_version_info.next_version(level_bump) + elif prerelease: + # we do at least a patch for prereleases + next_version_info = current_release_version_info.next_version("patch") else: - new_version = str( - semver.VersionInfo.parse(current_version).next_version(part=level_bump) - ) + next_version_info = current_release_version_info if prerelease: - logger.debug("Prerelease requested") - potentialy_prereleased_current_version = get_current_version( - prerelease_version=True - ) - if get_prerelease_pattern() in potentialy_prereleased_current_version: - logger.debug("Previouse prerelease detected, increment prerelease version") - prerelease_num = ( - int(potentialy_prereleased_current_version.split(".")[-1]) + 1 - ) + next_raw_version = next_version_info.to_tuple()[:3] + current_raw_version = current_version_info.to_tuple()[:3] + + if current_version_info.prerelease and next_raw_version == current_raw_version: + # next version (based on commits) matches current prerelease version + # bump prerelase + next_prerelease_version_info = current_version_info.bump_prerelease(config.get("prerelease_tag")) else: - logger.debug("No previouse prerelease detected, starting from 0") - prerelease_num = 0 - new_version = new_version + get_prerelease_pattern() + str(prerelease_num) + # next version (based on commits) higher than current prerelease version + # new prerelease based on next version + next_prerelease_version_info = next_version_info.bump_prerelease(config.get("prerelease_tag")) - return new_version + return str(next_prerelease_version_info) + else: + # normal version bump + return str(next_version_info) @LoggedFunction(logger) -def get_previous_version(version: str, omit_pattern: str = None) -> Optional[str]: +def get_previous_version(version: str) -> Optional[str]: """ Return the version prior to the given version. @@ -292,16 +332,55 @@ def get_previous_version(version: str, omit_pattern: str = None) -> Optional[str continue if found_version: - if omit_pattern and omit_pattern in commit_message: - continue - matches = re.match(r"v?(\d+.\d+.\d+)", commit_message) - if matches: + match = re.search(rf"{version_pattern}", commit_message) + if match: logger.debug(f"Version matches regex {commit_message}") - return matches.group(1).strip() + return match.group(1).strip() + + return get_last_version(pattern=version_pattern, skip_tags=[version, get_formatted_tag(version)]) + - return get_last_version( - [version, get_formatted_tag(version)], omit_pattern=omit_pattern - ) +@LoggedFunction(logger) +def get_previous_release_version(version: str) -> Optional[str]: + """ + Return the version prior to the given version. + + :param version: A string with the version number. + :return: A string with the previous version number. + """ + found_version = False + for commit_hash, commit_message in get_commit_log(): + logger.debug(f"Checking commit {commit_hash}") + if version in commit_message: + found_version = True + logger.debug(f'Found version in commit "{commit_message}"') + continue + + if found_version: + match = re.search(rf"{release_version_pattern}", commit_message) + if match: + logger.debug(f"Version matches regex {commit_message}") + return match.group(1).strip() + + return get_last_version(pattern=release_version_pattern, skip_tags=[version, get_formatted_tag(version)]) + + +@LoggedFunction(logger) +def get_current_release_version_by_commits() -> str: + """ + Return the current release version (NOT prerelease) version. + + :return: A string with the current version number. + """ + for commit_hash, commit_message in get_commit_log(): + logger.debug(f"Checking commit {commit_hash}") + match = re.search(rf"{release_version_pattern}", commit_message) + if match: + logger.debug(f"Version matches regex {commit_message}") + return match.group(1).strip() + + # nothing found, return the initial version + return "0.0.0" @LoggedFunction(logger) diff --git a/semantic_release/vcs_helpers.py b/semantic_release/vcs_helpers.py index a58d98480..9a6a72469 100644 --- a/semantic_release/vcs_helpers.py +++ b/semantic_release/vcs_helpers.py @@ -63,7 +63,7 @@ def get_commit_log(from_rev=None): @check_repo @LoggedFunction(logger) -def get_last_version(skip_tags=None, omit_pattern=None) -> Optional[str]: +def get_last_version(pattern, skip_tags=None) -> Optional[str]: """ Find the latest version using repo tags. @@ -77,29 +77,13 @@ def version_finder(tag): return tag.commit.committed_date for i in sorted(repo.tags, reverse=True, key=version_finder): - match = re.search(r"\d+\.\d+\.\d+", i.name) - if match: - # check if the omit pattern is present in the tag (e.g. -beta for pre-release tags) - if omit_pattern and omit_pattern in i.name: - continue - if i.name not in skip_tags: - return match.group(0) # Return only numeric vesion like 1.2.3 - - return None + if i.name in skip_tags: + continue + match = re.search(rf"{pattern}", i.name) + if match: + return match.group(0).strip() -@check_repo -@LoggedFunction(logger) -def get_version_from_tag(tag_name: str) -> Optional[str]: - """ - Get the git commit hash corresponding to a tag name. - - :param tag_name: Name of the git tag (i.e. 'v1.0.0') - :return: sha1 hash of the commit - """ - for i in repo.tags: - if i.name == tag_name: - return i.commit.hexsha return None diff --git a/setup.cfg b/setup.cfg index 7899cd137..a2c15540d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,5 +14,8 @@ force_grid_wrap=0 use_parentheses=True line_length=88 +[flake8] +max-line-length = 120 + [semantic_release] version_variable = semantic_release/__init__.py:__version__ diff --git a/tests/history/test_version.py b/tests/history/test_version.py index 0b4e2622e..5bffa36cb 100644 --- a/tests/history/test_version.py +++ b/tests/history/test_version.py @@ -13,6 +13,9 @@ get_current_version, get_current_version_by_config_file, get_current_version_by_tag, + get_current_release_version, + get_current_release_version_by_tag, + get_current_release_version_by_commits, get_new_version, get_previous_version, load_version_declarations, @@ -38,11 +41,20 @@ def test_current_version_should_return_correct_version(): assert get_current_version() == semantic_release.__version__ +def test_current_release_version_should_return_correct_version(): + assert get_current_release_version() == semantic_release.__version__ + + @mock.patch("semantic_release.history.get_last_version", return_value="last_version") def test_current_version_should_return_git_version(mock_last_version): assert "last_version" == get_current_version_by_tag() +@mock.patch("semantic_release.history.get_last_version", return_value="last_version") +def test_current_release_version_should_return_git_version(mock_last_version): + assert "last_version" == get_current_release_version_by_tag() + + @mock.patch( "semantic_release.history.config.get", wrapped_config_get(version_source="tag") ) @@ -68,6 +80,15 @@ def test_current_version_should_run_with_tag_only(mocker): assert not mock_get_current_version_by_config_file.called +@mock.patch("semantic_release.history.get_last_version", return_value=None) +@mock.patch("semantic_release.history.get_current_release_version_by_commits", return_value="0.0.0") +def test_current_release_version_should_return_default_version( + mock_last_version, + mock_current_release_version_by_commits +): + assert "0.0.0" == get_current_release_version() + + class TestGetPreviousVersion: @mock.patch( "semantic_release.history.get_commit_log", @@ -78,7 +99,7 @@ def test_should_return_correct_version(self): @mock.patch( "semantic_release.history.get_commit_log", - lambda: [("211", "0.10.0"), ("13", "0.9.0")], + lambda: [("211", "v0.10.0"), ("13", "v0.9.0")], ) def test_should_return_correct_version_with_v(self): assert get_previous_version("0.10.0") == "0.9.0" @@ -95,42 +116,82 @@ def test_should_return_correct_version_from_prerelease(self): lambda: [("211", "0.10.0"), ("13", "0.10.0-beta"), ("13", "0.9.0")], ) def test_should_return_correct_version_skip_prerelease(self): - assert get_previous_version("0.10.0-beta", omit_pattern="-beta") == "0.9.0" + assert get_previous_version("0.10.0-beta") == "0.9.0" + + +class TestGetCurrentReleaseVersionByCommits: + @mock.patch( + "semantic_release.history.get_commit_log", + lambda: [("211", "0.10.0-beta.1"), ("13", "0.9.1-beta.1"), ("13", "0.9.0")], + ) + def test_should_return_correct_version(self): + assert get_current_release_version_by_commits() == "0.9.0" + + @mock.patch( + "semantic_release.history.get_commit_log", + lambda: [("211", "v0.10.0-beta.1"), ("13", "0.9.1-beta.1"), ("13", "v0.9.0")], + ) + def test_should_return_correct_version_with_v(self): + assert get_current_release_version_by_commits() == "0.9.0" + + @mock.patch( + "semantic_release.history.get_commit_log", + lambda: [("211", "0.10.0-beta.0"), ("13", "0.9.0")], + ) + def test_should_return_correct_version_from_prerelease(self): + assert get_current_release_version_by_commits() == "0.9.0" + + @mock.patch( + "semantic_release.history.get_commit_log", + lambda: [("211", "7.28.0"), ("13", "7.27.0")], + ) + def test_should_return_correct_version_without_prerelease(self): + assert get_current_release_version_by_commits() == "7.28.0" class TestGetNewVersion: def test_major_bump(self): - assert get_new_version("0.0.0", "major") == "1.0.0" - assert get_new_version("0.1.0", "major") == "1.0.0" - assert get_new_version("0.1.9", "major") == "1.0.0" - assert get_new_version("10.1.0", "major") == "11.0.0" + assert get_new_version("0.0.0", "0.0.0", "major") == "1.0.0" + assert get_new_version("0.1.0", "0.1.0", "major") == "1.0.0" + assert get_new_version("0.1.9", "0.1.9", "major") == "1.0.0" + assert get_new_version("10.1.0", "10.1.0", "major") == "11.0.0" def test_minor_bump(self): - assert type(get_new_version("0.0.0", "minor")) is str - assert get_new_version("0.0.0", "minor") == "0.1.0" - assert get_new_version("1.2.0", "minor") == "1.3.0" - assert get_new_version("1.2.1", "minor") == "1.3.0" - assert get_new_version("10.1.0", "minor") == "10.2.0" + assert type(get_new_version("0.0.0", "0.0.0", "minor")) is str + assert get_new_version("0.0.0", "0.0.0", "minor") == "0.1.0" + assert get_new_version("1.2.0", "1.2.0", "minor") == "1.3.0" + assert get_new_version("1.2.1", "1.2.1", "minor") == "1.3.0" + assert get_new_version("10.1.0", "10.1.0", "minor") == "10.2.0" def test_patch_bump(self): - assert get_new_version("0.0.0", "patch") == "0.0.1" - assert get_new_version("0.1.0", "patch") == "0.1.1" - assert get_new_version("10.0.9", "patch") == "10.0.10" + assert get_new_version("0.0.0", "0.0.0", "patch") == "0.0.1" + assert get_new_version("0.1.0", "0.1.0", "patch") == "0.1.1" + assert get_new_version("10.0.9", "10.0.9", "patch") == "10.0.10" def test_none_bump(self): - assert get_new_version("1.0.0", None) == "1.0.0" + assert get_new_version("1.0.0", "1.0.0", None) == "1.0.0" def test_prerelease(self): - assert get_new_version("1.0.0", None, True) == "1.0.0-beta.0" - assert get_new_version("1.0.0", "major", True) == "2.0.0-beta.0" - assert get_new_version("1.0.0", "minor", True) == "1.1.0-beta.0" - assert get_new_version("1.0.0", "patch", True) == "1.0.1-beta.0" + assert get_new_version("1.0.1-beta.1", "1.0.0", None, True) == "1.0.1-beta.2" + assert get_new_version("1.0.1-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1" + assert get_new_version("1.0.1-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1" + assert get_new_version("1.0.1-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.2" + + assert get_new_version("1.0.0", "1.0.0", None, True) == "1.0.1-beta.1" + assert get_new_version("1.0.0", "1.0.0", "major", True) == "2.0.0-beta.1" + assert get_new_version("1.0.0", "1.0.0", "minor", True) == "1.1.0-beta.1" + assert get_new_version("1.0.0", "1.0.0", "patch", True) == "1.0.1-beta.1" + + assert get_new_version("0.9.0-beta.1", "1.0.0", None, True) == "1.0.1-beta.1" + assert get_new_version("0.9.0-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1" + assert get_new_version("0.9.0-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1" + assert get_new_version("0.9.0-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.1" + + with pytest.raises(ValueError): + get_new_version("0.9.0", "1.0.0", None, True) - def test_prerelease_bump(self, mocker): - mocker.patch( - "semantic_release.history.get_current_version", return_value="1.0.0-beta.0" - ) - assert get_new_version("1.0.0", None, True) == "1.0.0-beta.1" + with pytest.raises(ValueError): + get_new_version("1.0.0", "0.9.0", None, True) @mock.patch( @@ -153,7 +214,7 @@ class TestVersionPattern: ( "path:__version__", Path("path"), - r'__version__ *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']', + r'__version__ *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']', ), ], ) @@ -166,7 +227,7 @@ def test_from_variable(self, str, path, pattern): "str, path, pattern", [ ("path:pattern", Path("path"), r"pattern"), - ("path:Version: {version}", Path("path"), r"Version: (\d+\.\d+(?:\.\d+)?)"), + ("path:Version: {version}", Path("path"), r"Version: (\d+\.\d+\.\d+(-beta\.\d+)?)"), ], ) def test_from_pattern(self, str, path, pattern): @@ -353,7 +414,7 @@ def test_toml_replace(self, tmp_path, key, old_content, new_content): version_variable = "path:__version__" """, patterns=[ - (Path("path"), r'__version__ *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']'), + (Path("path"), r'__version__ *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'), ], ), dict( @@ -362,8 +423,8 @@ def test_toml_replace(self, tmp_path, key, old_content, new_content): version_variable = "path1:var1,path2:var2" """, patterns=[ - (Path("path1"), r'var1 *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']'), - (Path("path2"), r'var2 *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']'), + (Path("path1"), r'var1 *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'), + (Path("path2"), r'var2 *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'), ], ), dict( @@ -375,8 +436,8 @@ def test_toml_replace(self, tmp_path, key, old_content, new_content): ] """, patterns=[ - (Path("path1"), r'var1 *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']'), - (Path("path2"), r'var2 *[:=] *["\'](\d+\.\d+(?:\.\d+)?)["\']'), + (Path("path1"), r'var1 *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'), + (Path("path2"), r'var2 *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'), ], ), dict( diff --git a/tests/test_cli.py b/tests/test_cli.py index 1cd2e0735..feb4e1b40 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -48,12 +48,16 @@ def test_version_by_commit_should_call_correct_functions(mocker): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) mock_set_new_version.assert_called_once_with("2.0.0") mock_commit_new_version.assert_called_once_with("2.0.0") mock_tag_new_version.assert_called_once_with("2.0.0") @@ -83,12 +87,16 @@ def test_version_by_tag_with_commit_version_number_should_call_correct_functions mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) mock_set_new_version.assert_called_once_with("2.0.0") mock_commit_new_version.assert_called_once_with("2.0.0") mock_tag_new_version.assert_called_once_with("2.0.0") @@ -118,12 +126,16 @@ def test_version_by_tag_only_with_commit_version_number_should_call_correct_func mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once_with() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) assert not mock_set_new_version.called assert not mock_commit_new_version.called mock_tag_new_version.assert_called_once_with("2.0.0") @@ -145,12 +157,16 @@ def test_version_by_tag_should_call_correct_functions(mocker): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) mock_set_new_version.assert_called_once_with("2.0.0") mock_tag_new_version.assert_called_once_with("2.0.0") @@ -171,12 +187,16 @@ def test_version_by_tag_only_should_call_correct_functions(mocker): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once_with() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) assert not mock_set_new_version.called mock_tag_new_version.assert_called_once_with("2.0.0") @@ -198,12 +218,16 @@ def test_version_by_commit_without_tag_should_call_correct_functions(mocker): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", "major", False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False) mock_set_new_version.assert_called_once_with("2.0.0") mock_commit_new_version.assert_called_once_with("2.0.0") assert not mock_tag_new_version.called @@ -366,21 +390,28 @@ def test_print_version_no_change(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) print_version() outerr = capsys.readouterr() assert outerr.out == "" assert outerr.err == "No release will be made.\n" - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", None, False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False) def test_print_version_change(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="minor" ) @@ -390,7 +421,8 @@ def test_print_version_change(mocker, runner, capsys): assert outerr.out == "1.3.0" assert outerr.err == "" - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) @@ -398,16 +430,20 @@ def test_print_version_change_prerelease(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="minor" ) print_version(prerelease=True) outerr = capsys.readouterr() - assert outerr.out == "1.3.0-beta.0" + assert outerr.out == "1.3.0-beta.1" assert outerr.err == "" - mock_current_version.assert_called_once_with(True) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) @@ -415,8 +451,8 @@ def test_print_version_change_prerelease_bump(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) - mock_current_version = mocker.patch( - "semantic_release.history.get_current_version", return_value="1.3.0-beta.0" + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="minor" @@ -427,7 +463,8 @@ def test_print_version_change_prerelease_bump(mocker, runner, capsys): assert outerr.out == "1.3.0-beta.1" assert outerr.err == "" - mock_current_version.assert_called_once_with(prerelease_version=True) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) @@ -435,6 +472,9 @@ def test_print_version_force_major(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="major" ) @@ -444,7 +484,8 @@ def test_print_version_force_major(mocker, runner, capsys): assert outerr.out == "2.0.0" assert outerr.err == "" - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", "major") @@ -452,16 +493,20 @@ def test_print_version_force_major_prerelease(mocker, runner, capsys): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="major" ) print_version(force_level="major", prerelease=True) outerr = capsys.readouterr() - assert outerr.out == "2.0.0-beta.0" + assert outerr.out == "2.0.0-beta.1" assert outerr.err == "" mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", "major") @@ -478,12 +523,16 @@ def test_version_no_change(mocker, runner): mock_current_version = mocker.patch( "semantic_release.cli.get_current_version", return_value="1.2.3" ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) version() - mock_current_version.assert_called_once_with(False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() mock_evaluate_bump.assert_called_once_with("1.2.3", None) - mock_new_version.assert_called_once_with("1.2.3", None, False) + mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False) assert not mock_set_new_version.called assert not mock_commit_new_version.called assert not mock_tag_new_version.called @@ -595,8 +644,11 @@ def test_version_retry_and_giterror(mocker): def test_version_retry(mocker): - mock_get_current = mocker.patch( - "semantic_release.cli.get_current_version", return_value="current" + mock_current_version = mocker.patch( + "semantic_release.cli.get_current_version", return_value="1.2.3" + ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" ) mock_evaluate_bump = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="patch" @@ -609,9 +661,10 @@ def test_version_retry(mocker): result = version(noop=False, retry=True, force_level=False) assert result - mock_get_current.assert_called_once_with(False) - mock_evaluate_bump.assert_called_once_with("current", False) - mock_get_new.assert_called_once_with("current", "patch", False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() + mock_evaluate_bump.assert_called_once_with("1.2.3", False) + mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False) def test_publish_should_not_run_pre_commit_by_default(mocker): @@ -888,8 +941,11 @@ def test_publish_retry_version_fail(mocker): mock_get_current = mocker.patch( "semantic_release.cli.get_current_version", return_value="current" ) - mock_get_previous = mocker.patch( - "semantic_release.cli.get_previous_version", return_value="previous" + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) + get_previous_release_version = mocker.patch( + "semantic_release.cli.get_previous_release_version", return_value="previous" ) mock_get_owner_name = mocker.patch( "semantic_release.cli.get_repository_owner_and_name", @@ -904,8 +960,9 @@ def test_publish_retry_version_fail(mocker): publish(noop=False, retry=True, force_level=False) - mock_get_current.assert_called_once_with(False) - mock_get_previous.assert_called_once_with("current") + mock_get_current.assert_called_once() + mock_current_release_version.assert_called_once() + get_previous_release_version.assert_called_once_with("current") mock_get_owner_name.assert_called_once_with() mock_ci_check.assert_called() mock_checkout.assert_called_once_with("my_branch") @@ -918,8 +975,11 @@ def test_publish_bad_token(mocker): mock_get_current = mocker.patch( "semantic_release.cli.get_current_version", return_value="current" ) - mock_get_previous = mocker.patch( - "semantic_release.cli.get_previous_version", return_value="previous" + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" + ) + get_previous_release_version = mocker.patch( + "semantic_release.cli.get_previous_release_version", return_value="previous" ) mock_get_owner_name = mocker.patch( "semantic_release.cli.get_repository_owner_and_name", @@ -950,8 +1010,9 @@ def test_publish_bad_token(mocker): publish(noop=False, retry=True, force_level=False) - mock_get_current.assert_called_once_with(False) - mock_get_previous.assert_called_once_with("current") + mock_get_current.assert_called_once() + mock_current_release_version.assert_called_once() + get_previous_release_version.assert_called_once_with("current") mock_get_owner_name.assert_called_once_with() mock_ci_check.assert_called() mock_checkout.assert_called_once_with("my_branch") @@ -971,8 +1032,11 @@ def test_publish_bad_token(mocker): def test_publish_giterror_when_posting(mocker): - mock_get_current = mocker.patch( - "semantic_release.cli.get_current_version", return_value="current" + mock_current_version = mocker.patch( + "semantic_release.cli.get_current_version", return_value="1.2.3" + ) + mock_current_release_version = mocker.patch( + "semantic_release.cli.get_current_release_version", return_value="1.2.3" ) mock_evaluate = mocker.patch( "semantic_release.cli.evaluate_version_bump", return_value="patch" @@ -1024,14 +1088,15 @@ def test_publish_giterror_when_posting(mocker): publish(noop=False, retry=False, force_level=False) - mock_get_current.assert_called_once_with(False) - mock_evaluate.assert_called_once_with("current", False) - mock_get_new.assert_called_once_with("current", "patch", False) + mock_current_version.assert_called_once() + mock_current_release_version.assert_called_once() + mock_evaluate.assert_called_once_with("1.2.3", False) + mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False) mock_get_owner_name.assert_called_once_with() mock_ci_check.assert_called() mock_checkout.assert_called_once_with("my_branch") mock_should_bump_version.assert_called_once_with( - current_version="current", new_version="new", noop=False, retry=False + current_version="1.2.3", new_version="new", noop=False, retry=False ) mock_update_changelog_file.assert_called_once_with("new", "super md changelog") mock_bump_version.assert_called_once_with("new", "patch") @@ -1045,14 +1110,14 @@ def test_publish_giterror_when_posting(mocker): domain="domain", ) mock_check_token.assert_called_once_with() - mock_generate.assert_called_once_with("current") + mock_generate.assert_called_once_with("1.2.3") mock_markdown.assert_called_once_with( "owner", "name", "new", "super changelog", header=False, - previous_version="current", + previous_version="1.2.3", ) mock_post.assert_called_once_with("owner", "name", "new", "super md changelog") diff --git a/tests/test_vcs_helpers.py b/tests/test_vcs_helpers.py index 1fbabf950..aa2e02880 100644 --- a/tests/test_vcs_helpers.py +++ b/tests/test_vcs_helpers.py @@ -6,6 +6,7 @@ from git import GitCommandError, Repo, TagObject from semantic_release.errors import GitError, HvcsRepoParseError +from semantic_release.history import version_pattern, release_version_pattern from semantic_release.vcs_helpers import ( check_repo, checkout, @@ -14,7 +15,6 @@ get_current_head_hash, get_last_version, get_repository_owner_and_name, - get_version_from_tag, push_new_version, tag_new_version, update_additional_files, @@ -310,14 +310,14 @@ def test_checkout_should_checkout_correct_branch(mock_git): @pytest.mark.parametrize( - "skip_tags,expected_result", + "pattern, skip_tags,expected_result", [ - (None, "2.0.0"), - (["v2.0.0"], "1.1.0"), - (["v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None), + ("\d+.\d+.\d+", None, "2.0.0"), + ("\d+.\d+.\d+", ["v2.0.0"], "1.1.0"), + ("\d+.\d+.\d+", ["v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None), ], ) -def test_get_last_version(skip_tags, expected_result): +def test_get_last_version(pattern, skip_tags, expected_result): class FakeCommit: def __init__(self, com_date): self.committed_date = com_date @@ -345,18 +345,22 @@ def __init__(self, name, sha, date, is_tag_object): FakeTag("v1.0.0", "bbbbbbbbbbbbbbbbbbbb", 2, False), ] ) - assert expected_result == get_last_version(skip_tags) - + assert expected_result == get_last_version(pattern, skip_tags) @pytest.mark.parametrize( - "skip_tags,expected_result", + "pattern, skip_tags,expected_result", [ - (None, "2.0.0"), - (["v2.0.0"], "1.1.0"), - (["v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None), + (version_pattern, None, "2.1.0-beta.0"), + (version_pattern, ["v2.1.0-beta.0"], "2.0.0"), + (version_pattern, ["v2.1.0-beta.0", "v2.0.0-beta.0", "v2.0.0"], "1.1.0"), + (version_pattern, ["v2.1.0-beta.0", "v2.0.0-beta.0", "v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None), + (release_version_pattern, None, "2.0.0"), + (release_version_pattern, ["v2.0.0"], "1.1.0"), + (release_version_pattern, ["v2.0.0", "v1.1.0", "v1.0.0", "v0.1.0"], None), ], ) -def test_get_last_version_with_omit_pattern(skip_tags, expected_result): +def test_get_last_version_with_real_pattern(pattern, skip_tags, expected_result): + # TODO: add some prerelease tags class FakeCommit: def __init__(self, com_date): self.committed_date = com_date @@ -379,43 +383,14 @@ def __init__(self, name, sha, date, is_tag_object): return_value=[ FakeTag("v0.1.0", "aaaaaaaaaaaaaaaaaaaa", 1, True), FakeTag("v2.0.0", "dddddddddddddddddddd", 5, True), - FakeTag("v2.1.0-beta", "ffffffffffffffffffff", 7, True), + FakeTag("v2.1.0-beta.0", "ffffffffffffffffffff", 7, True), FakeTag("badly_formatted", "eeeeeeeeeeeeeeeeeeee", 6, False), - FakeTag("v2.0.0-beta", "ffffffffffffffffffff", 4, True), + FakeTag("v2.0.0-beta.0", "ffffffffffffffffffff", 4, True), FakeTag("v1.1.0", "cccccccccccccccccccc", 3, True), FakeTag("v1.0.0", "bbbbbbbbbbbbbbbbbbbb", 2, False), ] ) - assert expected_result == get_last_version(skip_tags, omit_pattern="-beta") - - -@pytest.mark.parametrize( - "tag_name,expected_version", - [ - ("v0.1.0", "aaaaa"), - ("v1.0.0", "bbbbb"), - ("v2.0.0", None), - ], -) -def test_get_version_from_tag(tag_name, expected_version): - class FakeCommit: - def __init__(self, sha): - self.hexsha = sha - - class FakeTag: - def __init__(self, name, sha): - self.name = name - self.commit = FakeCommit(sha) - - mock.patch("semantic_release.vcs_helpers.check_repo") - git.repo.base.Repo.tags = mock.PropertyMock( - return_value=[ - FakeTag("v0.1.0", "aaaaa"), - FakeTag("v1.0.0", "bbbbb"), - FakeTag("v1.1.0", "ccccc"), - ] - ) - assert expected_version == get_version_from_tag(tag_name) + assert expected_result == get_last_version(pattern, skip_tags) def test_update_changelog_file_ok(mock_git, mocker):