diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 62b3aec971..39efdea814 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -46,6 +46,10 @@ def is_python(self) -> bool: def is_rust_cargo(self) -> bool: return os.path.isfile("Cargo.toml") + @property + def is_maven_project(self) -> bool: + return os.path.isfile("pom.xml") + @property def is_npm_package(self) -> bool: return os.path.isfile("package.json") @@ -222,6 +226,7 @@ def _ask_version_provider(self) -> str: "commitizen": "commitizen: Fetch and set version in commitizen config (default)", "cargo": "cargo: Get and set version from Cargo.toml:project.version field", "composer": "composer: Get and set version from composer.json:project.version field", + "mvn": "mvn: Get and set version from pom.xml:project.version field", "npm": "npm: Get and set version from package.json:project.version field", "pep621": "pep621: Get and set version from pyproject.toml:project.version field", "poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field", @@ -236,6 +241,8 @@ def _ask_version_provider(self) -> str: default_val = "pep621" elif self.project_info.is_rust_cargo: default_val = "cargo" + elif self.project_info.is_maven_project: + default_val = "mvn" elif self.project_info.is_npm_package: default_val = "npm" elif self.project_info.is_php_composer: diff --git a/commitizen/providers/__init__.py b/commitizen/providers/__init__.py index 51302d2b37..5d992ee816 100644 --- a/commitizen/providers/__init__.py +++ b/commitizen/providers/__init__.py @@ -11,6 +11,7 @@ from commitizen.providers.cargo_provider import CargoProvider from commitizen.providers.commitizen_provider import CommitizenProvider from commitizen.providers.composer_provider import ComposerProvider +from commitizen.providers.mvn_provider import MavenProvider from commitizen.providers.npm_provider import NpmProvider from commitizen.providers.pep621_provider import Pep621Provider from commitizen.providers.poetry_provider import PoetryProvider @@ -21,6 +22,7 @@ "CargoProvider", "CommitizenProvider", "ComposerProvider", + "MavenProvider", "NpmProvider", "Pep621Provider", "PoetryProvider", diff --git a/commitizen/providers/mvn_provider.py b/commitizen/providers/mvn_provider.py new file mode 100644 index 0000000000..d7363914f6 --- /dev/null +++ b/commitizen/providers/mvn_provider.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import subprocess + +from commitizen.providers.base_provider import VersionProvider + + +class MavenProvider(VersionProvider): + """ + Maven version management + + ref: https://octopus.com/blog/maven-versioning-explained + + Major.Minor.Patch-BuildNumber-Qualifier + + Precedence: + - alpha or a + - beta or b + - milestone or m + - rc or cr + - snapshot + - (the empty string) or ga or final or release + - sp + """ + + FULL_VERSION_REGEX = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)[\.-]?(?P<buildnumber>\d+)?[\.-]?(?P<qualifier>\w+)?" + + TAG_FORMAT_REGEXS = { + "$version": r"(?P<version>.+)", + "$major": r"(?P<major>\d+)", + "$minor": r"(?P<minor>\d+)", + "$patch": r"(?P<patch>\d+)", + "$buildnumber": r"(?P<buildnumber>\d+)?", + "$qualifier": r"(?P<qualifier>\w+)?", + } + + filename = "./pom.xml" + + def __run_cmd(self, cmd) -> str: + return ( + subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + .stdout.read() # type: ignore + .decode("utf-8") + .strip() + ) + + def get_version(self, file: str = filename) -> str: + return self.__run_cmd( + f"mvn help:evaluate -Dexpression=project.version -q -DforceStdout -f {file}" + ) + + def set_version(self, version: str, file: str = filename) -> None: + self.__run_cmd(f"mvn versions:set -DnewVersion={version} -f {file}") diff --git a/pyproject.toml b/pyproject.toml index b644753b1b..251a957f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,7 @@ restructuredtext = "commitizen.changelog_formats.restructuredtext:RestructuredTe cargo = "commitizen.providers:CargoProvider" commitizen = "commitizen.providers:CommitizenProvider" composer = "commitizen.providers:ComposerProvider" +mvn = "commitizen.providers:MavenProvider" npm = "commitizen.providers:NpmProvider" pep621 = "commitizen.providers:Pep621Provider" poetry = "commitizen.providers:PoetryProvider" diff --git a/tests/data/sample_pom.xml b/tests/data/sample_pom.xml new file mode 100644 index 0000000000..50b9f372bd --- /dev/null +++ b/tests/data/sample_pom.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- PARENT --> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.1.0</version> + <relativePath /> + </parent> + + <!-- BASICS --> + <groupId>io.github.commitizen-tools</groupId> + <artifactId>sample-maven-pom</artifactId> + <version>3.2.1</version> + <packaging>pom</packaging> + + <!-- DESCRIPTION --> + <name>Sample Maven POM</name> + <description>This is a sample POM</description> + + <scm> + <url>${app.url}</url> + <tag>v@{project.version}</tag> + </scm> + + <!-- PROPS / VERSIONS --> + <properties> + <!-- App --> + <app.url>hthttps://github.com/commitizen-tools/commitizen</app.url> + + <!-- Java --> + <java.version>17</java.version> + + <!-- Spring --> + <spring-boot.version>3.1.0</spring-boot.version> + </properties> + + <!-- BOM DEPS --> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <version>${spring-boot.version}</version> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + <version>${spring-boot.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>${spring-boot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <version>${spring-boot.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + +</project> diff --git a/tests/data/sample_pom_snapshot.xml b/tests/data/sample_pom_snapshot.xml new file mode 100644 index 0000000000..694683b0fa --- /dev/null +++ b/tests/data/sample_pom_snapshot.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- PARENT --> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.1.0</version> + <relativePath /> + </parent> + + <!-- BASICS --> + <groupId>io.github.commitizen-tools</groupId> + <artifactId>sample-maven-pom</artifactId> + <version>3.2.1-SNAPSHOT</version> + <packaging>pom</packaging> + + <!-- DESCRIPTION --> + <name>Sample Maven POM</name> + <description>This is a sample POM</description> + + <scm> + <url>${app.url}</url> + <tag>v@{project.version}</tag> + </scm> + + <!-- PROPS / VERSIONS --> + <properties> + <!-- App --> + <app.url>hthttps://github.com/commitizen-tools/commitizen</app.url> + + <!-- Java --> + <java.version>17</java.version> + + <!-- Spring --> + <spring-boot.version>3.1.0</spring-boot.version> + </properties> + + <!-- BOM DEPS --> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <version>${spring-boot.version}</version> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + <version>${spring-boot.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>${spring-boot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <version>${spring-boot.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + +</project> diff --git a/tests/providers/test_mvn_provider.py b/tests/providers/test_mvn_provider.py new file mode 100644 index 0000000000..014831de6a --- /dev/null +++ b/tests/providers/test_mvn_provider.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest + +import os + +from commitizen.config.base_config import BaseConfig +from commitizen.providers.mvn_provider import MavenProvider + + +def test_can_run_subcommand(config: BaseConfig): + provider = MavenProvider(config) + got = provider._MavenProvider__run_cmd("echo 'hi'") # type: ignore + expected = "hi" + assert got == expected + + +@pytest.mark.parametrize( + "file, expected", + ( + ("./tests/data/sample_pom.xml", "3.2.1"), + ("./tests/data/sample_pom_snapshot.xml", "3.2.1-SNAPSHOT"), + ), +) +def test_get_version(config: BaseConfig, file: str, expected: str): + provider = MavenProvider(config) + got = provider.get_version(file) + assert got == expected + + +def test_set_version(config: BaseConfig): + provider = MavenProvider(config) + file = "./tests/data/sample_pom.xml" + expected = "3.2.2" + provider.set_version(expected, file) + got = provider.get_version(file) + assert got == expected + + # rollback changes + expected = "3.2.1" + provider.set_version(expected, file) + got = provider.get_version(file) + assert got == expected + + # delete backup file created + backup_file = file + ".versionsBackup" + assert os.path.exists(backup_file) + if os.path.exists(backup_file): + os.remove(backup_file) + assert not os.path.exists(backup_file)