diff --git a/dev/archery/archery/crossbow/core.py b/dev/archery/archery/crossbow/core.py index e88a08445cdf..33bd5f6f88b2 100644 --- a/dev/archery/archery/crossbow/core.py +++ b/dev/archery/archery/crossbow/core.py @@ -15,19 +15,20 @@ # specific language governing permissions and limitations # under the License. -import os -import re import fnmatch import glob -import time import logging import mimetypes +import os +import re +import subprocess import textwrap +import time import uuid +import warnings +from datetime import date from io import StringIO from pathlib import Path -from datetime import date -import warnings import jinja2 from ruamel.yaml import YAML @@ -705,26 +706,36 @@ def put(self, job, prefix='build', increment_job_id=True): return self.create_branch(job.branch, files=job.render_files()) -def get_version(root, **kwargs): +def get_version(root): """ - Parse function for setuptools_scm that ignores tags for non-C++ - subprojects, e.g. apache-arrow-js-XXX tags. + Calculate a development version from the latest Arrow C++ release tag. """ - from setuptools_scm.git import parse as parse_git_version - from setuptools_scm import Configuration - - # query the calculated version based on the git tags - kwargs['describe_command'] = ( - 'git describe --dirty --tags --long --match "apache-arrow-[0-9]*.*"' + result = subprocess.run( + [ + "git", + "describe", + "--dirty", + "--tags", + "--long", + "--match", + "apache-arrow-[0-9]*.*", + ], + cwd=root, + check=True, + stdout=subprocess.PIPE, + text=True, ) - - # Create a Configuration object with necessary parameters - config = Configuration( - git_describe_command=kwargs['describe_command'] + description = result.stdout.strip() + describe_match = re.fullmatch( + r"apache-arrow-(?P.+)-(?P\d+)" + r"-g[0-9a-f]+(?:-dirty)?", + description, ) - - version = parse_git_version(root, config=config, **kwargs) - tag = str(version.tag) + if describe_match is None: + raise CrossbowError( + f"Unable to parse git describe output: {description!r}" + ) + tag = describe_match.group("tag") # We may get a development tag for the next version, such as "5.0.0.dev0", # or the tag of an already released version, such as "4.0.0". @@ -733,11 +744,14 @@ def get_version(root, **kwargs): # 4.0.0 is 5.0.0). pattern = r"^(\d+)\.(\d+)\.(\d+)" match = re.match(pattern, tag) + if match is None: + raise CrossbowError(f"Unable to parse Arrow version tag: {tag!r}") major, minor, patch = map(int, match.groups()) if 'dev' not in tag: major += 1 - return f"{major}.{minor}.{patch}.dev{version.distance or 0}" + distance = int(describe_match.group("distance")) + return f"{major}.{minor}.{patch}.dev{distance}" class Serializable: diff --git a/dev/archery/archery/crossbow/tests/test_core.py b/dev/archery/archery/crossbow/tests/test_core.py index 9a38ca75d7f5..fb4d5fdc59e7 100644 --- a/dev/archery/archery/crossbow/tests/test_core.py +++ b/dev/archery/archery/crossbow/tests/test_core.py @@ -17,9 +17,17 @@ from archery.utils.source import ArrowSources from archery.crossbow import Config, Queue -from archery.crossbow.core import CrossbowError, Repo, TaskAssets, TaskStatus +from archery.crossbow.core import ( + CrossbowError, + Repo, + TaskAssets, + TaskStatus, + get_version, +) import pathlib +import shutil +import subprocess from datetime import date from unittest import mock @@ -27,6 +35,84 @@ from github import GithubException +@pytest.mark.parametrize( + ("description", "expected"), + [ + ("apache-arrow-4.0.0-0-gabcdef\n", "5.0.0.dev0"), + ("apache-arrow-4.0.0-12-gabcdef-dirty\n", "5.0.0.dev12"), + ("apache-arrow-5.0.0.dev-7-gabcdef\n", "5.0.0.dev7"), + ("apache-arrow-5.0.0-rc1-2-gabcdef\n", "6.0.0.dev2"), + ], +) +def test_get_version(description, expected): + with mock.patch("archery.crossbow.core.subprocess.run") as mocked_run: + mocked_run.return_value.stdout = description + + assert get_version("/arrow") == expected + + mocked_run.assert_called_once_with( + [ + "git", + "describe", + "--dirty", + "--tags", + "--long", + "--match", + "apache-arrow-[0-9]*.*", + ], + cwd="/arrow", + check=True, + stdout=mock.ANY, + text=True, + ) + + +def test_get_version_rejects_unexpected_describe_output(): + with mock.patch("archery.crossbow.core.subprocess.run") as mocked_run: + mocked_run.return_value.stdout = "not-an-arrow-version\n" + + with pytest.raises(CrossbowError, match="git describe output"): + get_version("/arrow") + + +@pytest.mark.skipif(shutil.which("git") is None, reason="git is required") +def test_get_version_from_git_repository(tmp_path): + def run_git(*args): + subprocess.run( + ["git", *args], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + + run_git("init") + run_git( + "-c", + "user.name=Archery Test", + "-c", + "user.email=archery@example.com", + "commit", + "--allow-empty", + "-m", + "release", + ) + run_git("tag", "apache-arrow-4.0.0") + + run_git( + "-c", + "user.name=Archery Test", + "-c", + "user.email=archery@example.com", + "commit", + "--allow-empty", + "-m", + "development", + ) + + assert get_version(tmp_path) == "5.0.0.dev1" + + def test_config(): src = ArrowSources.find() conf = Config.load_yaml(src.dev / "tasks" / "tasks.yml") diff --git a/dev/archery/setup.py b/dev/archery/setup.py index 66d04d692fd9..0b2bbfcb89c6 100755 --- a/dev/archery/setup.py +++ b/dev/archery/setup.py @@ -30,7 +30,7 @@ extras = { 'benchmark': ['pandas'], 'crossbow': [jinja_req, 'pygit2>=1.14.0', 'pygithub>=2.5.0', 'requests', - 'ruamel.yaml', 'setuptools_scm>=8.0.0'], + 'ruamel.yaml'], 'docker': ['ruamel.yaml', 'python-dotenv'], 'integration': ['cffi', 'numpy'], 'integration-java': ['jpype1'],