diff --git a/semver/__init__.py b/semver/__init__.py index ae27fa8..bda8f5c 100644 --- a/semver/__init__.py +++ b/semver/__init__.py @@ -12,30 +12,17 @@ from semver.scm import SCM from semver.scm.git import Git from semver.semver import SemVer +from semver.utils import setting_to_array from semver.exceptions import ( NoMergeFoundException, NotMainBranchException, NoGitFlowException, - SemverException, ) version = "0.0.0" -def _setting_to_array(setting) -> List[str]: - """ - Get a setting from the config file and return it as a list - :param setting: The setting to get from the config file - :return: The setting as a list - """ - config: dict = toml.load("./.bumpversion.cfg") - semver: dict = config.get("semver", {}) - value: str = semver.get(setting, "") - - return [v.strip() for v in value.split(",") if v.strip()] - - def main(): """Main entry point for the application""" parser = argparse.ArgumentParser(description="Bump Semantic Version.") @@ -63,10 +50,10 @@ def main(): app = SemVer( scm=scm, - main_branches=_setting_to_array("main_branches"), - major_branches=_setting_to_array("major_branches"), - minor_branches=_setting_to_array("minor_branches"), - patch_branches=_setting_to_array("patch_branches"), + main_branches=setting_to_array("main_branches"), + major_branches=setting_to_array("major_branches"), + minor_branches=setting_to_array("minor_branches"), + patch_branches=setting_to_array("patch_branches"), ) if args.debug: diff --git a/semver/get_version.py b/semver/get_version.py index 6c3e195..38e9921 100644 --- a/semver/get_version.py +++ b/semver/get_version.py @@ -1,62 +1,57 @@ import argparse -import re -import subprocess -from semver.logger import logging, logger, console_logger -from semver.utils import get_tag_version, get_file_version, DEVNULL + +from semver.logger import logging, console_logger from semver import SemVer -from semver.bump import bump_version - -def get_version(build=0,version_format=None,dot=False): - version = get_tag_version() - - # Get the commit hash of the version - v_hash = subprocess.Popen(['git', 'rev-list', '-n', '1', version], stdout=subprocess.PIPE, - stderr=DEVNULL, cwd='.').stdout.read().decode('utf-8').rstrip() - # Get the current commit hash - c_hash = subprocess.Popen(['git', 'rev-parse', 'HEAD'], stdout=subprocess.PIPE, - stderr=DEVNULL, cwd='.').stdout.read().decode('utf-8').rstrip() - - # If the version commit hash and current commit hash - # do not match return the branch name else return the version - if v_hash != c_hash: - logger.debug("v_hash and c_hash do not match!") - branch = subprocess.Popen(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stdout=subprocess.PIPE, - stderr=DEVNULL, cwd='.').stdout.read().decode('utf-8').rstrip() - semver = SemVer() - semver.merged_branch = branch - logger.debug("merged branch is: {}".format(semver.merged_branch)) - version_type = semver.get_version_type() - logger.debug("version type is: {}".format(version_type)) - if version_type: - - next_version = bump_version(get_tag_version(), version_type, False, False) - - if version_format in ('npm','docker'): - return "{}-{}.{}".format(next_version,re.sub(r'[/_]', '-', branch),build) - if version_format == 'maven': - qualifier = 'SNAPSHOT' if build == 0 else build - return "{}-{}-{}".format(next_version,re.sub(r'[/_]', '-', branch),qualifier) - if dot: - branch = branch.replace('/','.') - return branch - return version +from semver.utils import setting_to_array +from semver.scm.git import Git def main(): - parser = argparse.ArgumentParser(description='Get Version or Branch.') - parser.add_argument('-d', '--dot', help='Switch out / for . to be used in docker tag', action='store_true', dest='dot') - parser.add_argument('-D', '--debug', help='Sets logging level to DEBUG', action='store_true', dest='debug', default=False) - parser.add_argument('-f', '--format', help='Format for pre-release version syntax', choices=['npm','maven','docker'], default=None) - parser.add_argument('-b', '--build-number', help='Build number, used in pre-releases', default=0) - + parser = argparse.ArgumentParser(description="Get Version or Branch.") + parser.add_argument( + "-d", + "--dot", + help="Switch out / for . to be used in docker tag", + action="store_true", + dest="dot", + ) + parser.add_argument( + "-D", + "--debug", + help="Sets logging level to DEBUG", + action="store_true", + dest="debug", + default=False, + ) + parser.add_argument( + "-f", + "--format", + help="Format for pre-release version syntax", + choices=["npm", "maven", "docker"], + default=None, + ) + parser.add_argument( + "-b", "--build-number", help="Build number, used in pre-releases", default=0 + ) + args = parser.parse_args() if args.debug: console_logger.setLevel(logging.DEBUG) - print(get_version(args.build_number,args.format,args.dot)) + semver = SemVer( + scm=Git(), + main_branches=setting_to_array("main_branches"), + major_branches=setting_to_array("major_branches"), + minor_branches=setting_to_array("minor_branches"), + patch_branches=setting_to_array("patch_branches"), + ) -if __name__ == '__main__': - try: main() - except: raise + print(semver.get_version(args.build_number, args.format, args.dot)) + +if __name__ == "__main__": + try: + main() + except Exception as e: + raise e diff --git a/semver/scm/__init__.py b/semver/scm/__init__.py index 7d01403..e73d779 100644 --- a/semver/scm/__init__.py +++ b/semver/scm/__init__.py @@ -26,6 +26,14 @@ class SCM(ABC): def tag_version(self, version: str) -> None: raise NotImplementedError() + @abstractmethod + def get_version_hash(self, version: str) -> str: + raise NotImplementedError() + + @abstractmethod + def get_hash(self) -> str: + raise NotImplementedError() + def get_file_version(self, config: dict) -> str: """ :param config: The bumpversion config as a dict diff --git a/semver/scm/git.py b/semver/scm/git.py index 4cee797..ef71ade 100644 --- a/semver/scm/git.py +++ b/semver/scm/git.py @@ -106,8 +106,6 @@ class Git(SCM): branch: str = self.get_branch() - logger.info(f"Main branch is {branch}") - matches = self.git_commit_pattern.search( message.replace("\\n", "\n").replace("\\", "") ) @@ -140,4 +138,25 @@ class Git(SCM): ) def tag_version(self, version: str) -> None: + """ + Creates a git tag at HEAD with the given version + :param version: The version to tag + """ self._run_command(self.git_bin, "tag", version) + + def get_version_hash(self, version: str) -> str: + """ + Get the hash of the commit that has the given version + :param version: The version to get the hash for + :return: The hash of the commit that has the given version + """ + proc = self._run_command(self.git_bin, "rev-list", "-n", "1", version) + return proc.stdout.rstrip() + + def get_hash(self) -> str: + """ + Get the hash of the current commit + :return: The hash of the current commit + """ + proc = self._run_command(self.git_bin, "rev-parse", "HEAD") + return proc.stdout.rstrip() diff --git a/semver/scm/mock.py b/semver/scm/mock.py index ec26713..cd9fc51 100644 --- a/semver/scm/mock.py +++ b/semver/scm/mock.py @@ -18,3 +18,9 @@ class MockSCM(SCM): def tag_version(self, version: str) -> None: pass + + def get_version_hash(self, version: str) -> str: + return "HASH" + + def get_hash(self) -> str: + return "HASH" diff --git a/semver/scm/tests/test_git.py b/semver/scm/tests/test_git.py index 2547358..8760949 100644 --- a/semver/scm/tests/test_git.py +++ b/semver/scm/tests/test_git.py @@ -98,3 +98,20 @@ class TestMockSCM(unittest.TestCase): version = "1.0.0" self.scm.tag_version(version) + + @mock.patch("subprocess.run") + def test_get_version_hash(self, mock_subprocess_run: mock.Mock): + mock_subprocess_run.return_value.stdout = "HASH\n" + + version = "1.0.0" + expected_hash = "HASH" + version_hash = self.scm.get_version_hash(version) + self.assertEqual(version_hash, expected_hash) + + @mock.patch("subprocess.run") + def test_get_hash(self, mock_subprocess_run: mock.Mock): + mock_subprocess_run.return_value.stdout = "HASH\n" + + expected_hash = "HASH" + version_hash = self.scm.get_hash() + self.assertEqual(version_hash, expected_hash) diff --git a/semver/scm/tests/test_mock.py b/semver/scm/tests/test_mock.py index a934be0..e17de0a 100644 --- a/semver/scm/tests/test_mock.py +++ b/semver/scm/tests/test_mock.py @@ -32,3 +32,14 @@ class TestMockSCM(unittest.TestCase): def test_tag_version(self): version = "1.0.0" self.scm.tag_version(version) + + def test_get_version_hash(self): + version = "1.0.0" + expected_hash = "HASH" + version_hash = self.scm.get_version_hash(version) + self.assertEqual(version_hash, expected_hash) + + def test_get_hash(self): + expected_hash = "HASH" + version_hash = self.scm.get_hash() + self.assertEqual(version_hash, expected_hash) diff --git a/semver/semver.py b/semver/semver.py index 2dade33..ca1af58 100644 --- a/semver/semver.py +++ b/semver/semver.py @@ -1,5 +1,6 @@ from typing import List, Union from pathlib import Path +import re import toml @@ -139,6 +140,52 @@ class SemVer: f"Tried to version file: '{file_path}' but it doesn't exist!" ) + def get_version( + self, build: int = 0, version_format: Union[str, None] = None, dot: bool = False + ): + """ + Get the version of the repo + :param build: The build number + :param version_format: The format of the version + :param dot: Whether or not to replace / with . + :return: The version + """ + version = self._scm.get_tag_version() + + # Get the commit hash of the version + v_hash = self._scm.get_version_hash(version) + # Get the current commit hash + c_hash = self._scm.get_hash() + + # If the version commit hash and current commit hash + # do not match return the branch name else return the version + if v_hash != c_hash: + logger.debug("v_hash and c_hash do not match!") + branch = self._scm.get_branch() + logger.debug("merged branch is: {}".format(branch)) + version_type = self._scm.get_version_type( + branch, self._major_branches, self._minor_branches, self._patch_branches + ) + logger.debug("version type is: {}".format(version_type)) + if version_type: + next_version = self._bump_version( + self._scm.get_tag_version(), version_type, False, False + ) + + if version_format in ("npm", "docker"): + return "{}-{}.{}".format( + next_version, re.sub(r"[/_]", "-", branch), build + ) + if version_format == "maven": + qualifier = "SNAPSHOT" if build == 0 else build + return "{}-{}-{}".format( + next_version, re.sub(r"[/_]", "-", branch), qualifier + ) + if dot: + branch = branch.replace("/", ".") + return branch + return version + def run(self, push=True): """ Run the versioning process @@ -152,9 +199,9 @@ class SemVer: self._merged_branch = self._scm.get_merge_branch() if not self._merged_branch: - raise NoMergeFoundException() + raise NoMergeFoundException("No merge found") if self._branch not in self._main_branches: - raise NotMainBranchException() + raise NotMainBranchException("Not a main branch") self._version_type = self._scm.get_version_type( self._branch, @@ -164,7 +211,7 @@ class SemVer: ) if not self._version_type: - raise NoGitFlowException() + raise NoGitFlowException("Could not determine version type") self._version_repo() if push: diff --git a/semver/tests/test_semver.py b/semver/tests/test_semver.py index 5b857ab..554b85d 100644 --- a/semver/tests/test_semver.py +++ b/semver/tests/test_semver.py @@ -12,7 +12,7 @@ from semver.exceptions import ( ) -class TestSCM(unittest.TestCase): +class TestSemVer(unittest.TestCase): def setUp(self): scm = mock.MagicMock(MockSCM()) self.semver: SemVer = SemVer(scm=scm) @@ -142,3 +142,52 @@ class TestSCM(unittest.TestCase): with self.assertRaises(NoGitFlowException): self.semver.run() + + @mock.patch("semver.semver.SemVer._bump_version") + def test_get_version(self, mock_bump_version: mock.Mock): + self.semver._scm.get_branch.return_value = "feature/example" + self.semver._scm.get_tag_version.return_value = "1.0.0" + self.semver._scm.get_version_hash.return_value = "HASH" + self.semver._scm.get_hash.return_value = "ALT_HASH" + + mock_bump_version.return_value = "1.0.1" + + expected_version = "1.0.0+HASH" + version = self.semver.get_version(dot=True) + self.assertEqual(version, "feature.example") + + @mock.patch("semver.semver.SemVer._bump_version") + def test_get_version_docker(self, mock_bump_version: mock.Mock): + self.semver._scm.get_branch.return_value = "feature/example" + self.semver._scm.get_tag_version.return_value = "1.0.0" + self.semver._scm.get_version_hash.return_value = "HASH" + self.semver._scm.get_hash.return_value = "ALT_HASH" + + mock_bump_version.return_value = "1.0.1" + + expected_version = "1.0.0+HASH" + version = self.semver.get_version(version_format="docker") + self.assertEqual(version, "1.0.1-feature-example.0") + + @mock.patch("semver.semver.SemVer._bump_version") + def test_get_version_maven(self, mock_bump_version: mock.Mock): + self.semver._scm.get_branch.return_value = "feature/example" + self.semver._scm.get_tag_version.return_value = "1.0.0" + self.semver._scm.get_version_hash.return_value = "HASH" + self.semver._scm.get_hash.return_value = "ALT_HASH" + + mock_bump_version.return_value = "1.0.1" + + expected_version = "1.0.1-feature-example-SNAPSHOT" + version = self.semver.get_version(version_format="maven") + self.assertEqual(version, expected_version) + + def test_get_version_no_hash(self): + self.semver._scm.get_branch.return_value = "main" + self.semver._scm.get_tag_version.return_value = "1.0.0" + self.semver._scm.get_version_hash.return_value = "HASH" + self.semver._scm.get_hash.return_value = "HASH" + + expected_version = "1.0.0" + version = self.semver.get_version() + self.assertEqual(version, expected_version) diff --git a/semver/tests/test_utils.py b/semver/tests/test_utils.py new file mode 100644 index 0000000..47c4bfb --- /dev/null +++ b/semver/tests/test_utils.py @@ -0,0 +1,59 @@ +import unittest +from unittest import mock + +from semver.utils import get_settings, setting_to_array +from semver.exceptions import SemverException + + +class TestUtils(unittest.TestCase): + @mock.patch("toml.load") + @mock.patch("pathlib.Path.is_file") + def test_get_settings_toml(self, mock_is_file: mock.Mock, mock_toml: mock.Mock): + get_settings.cache_clear() + + mock_is_file.side_effect = [True, False] + + mock_toml.return_value = {"1": {"a": "alpha", "fruit": "apple"}} + settings = get_settings() + self.assertEqual(settings, {"1": {"a": "alpha", "fruit": "apple"}}) + + @mock.patch("configparser.ConfigParser") + @mock.patch("pathlib.Path.is_file") + def test_get_settings_cfg( + self, mock_is_file: mock.Mock, mock_config_parser: mock.Mock + ): + get_settings.cache_clear() + + mock_is_file.side_effect = [False, True] + + mock_config_parser.return_value.read.return_value = ["./.bumpversion.cfg"] + mock_config_parser.return_value.sections.return_value = ["1", "2", "3"] + mock_config_parser.return_value.items.side_effect = [ + [("a", "alpha"), ("fruit", "apple")], + [("b", "bravo"), ("fruit", "banana")], + [("c", "charlie"), ("fruit", "cherry")], + ] + + settings = get_settings() + self.assertEqual( + settings, + { + "1": {"a": "alpha", "fruit": "apple"}, + "2": {"b": "bravo", "fruit": "banana"}, + "3": {"c": "charlie", "fruit": "cherry"}, + }, + ) + + @mock.patch("pathlib.Path.is_file") + def test_get_settings_no_file(self, mock_is_file: mock.Mock): + get_settings.cache_clear() + + mock_is_file.side_effect = [False, False] + with self.assertRaises(SemverException): + get_settings() + + @mock.patch("semver.utils.get_settings") + def test_setting_to_array(self, mock_get_settings: mock.Mock): + mock_get_settings.return_value = {"semver": {"test": "test1, test2"}} + settings = setting_to_array("test") + self.assertEqual(settings, ["test1", "test2"]) diff --git a/semver/utils.py b/semver/utils.py new file mode 100644 index 0000000..82955b8 --- /dev/null +++ b/semver/utils.py @@ -0,0 +1,38 @@ +from typing import List +from pathlib import Path +from functools import cache +import configparser + +import toml + +from semver.exceptions import SemverException + + +@cache +def get_settings() -> dict: + """ + Get the settings from the config file + :return: The settings from the config file + """ + if Path("./.bumpversion.toml").is_file(): + return toml.load("./.bumpversion.toml") + if Path("./.bumpversion.cfg").is_file(): + config = configparser.ConfigParser() + config.read("./.bumpversion.cfg") + + return {section: dict(config.items(section)) for section in config.sections()} + + raise SemverException("No config file found") + + +def setting_to_array(setting) -> List[str]: + """ + Get a setting from the config file and return it as a list + :param setting: The setting to get from the config file + :return: The setting as a list + """ + config: dict = get_settings() + semver: dict = config.get("semver", {}) + value: str = semver.get(setting, "") + + return [v.strip() for v in value.split(",") if v.strip()]