summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGreg Becker <becker33@llnl.gov>2022-06-27 18:54:41 -0700
committerGitHub <noreply@github.com>2022-06-28 01:54:41 +0000
commitdf44045fdb68e429bd1accdb7e7c9b947521d75c (patch)
treecb03704f83a285baaaa21c2522ee51ea49c3ce99 /lib
parent7dd2ca02077dc15b3a79d9c6ec990973cc46eb7d (diff)
downloadspack-df44045fdb68e429bd1accdb7e7c9b947521d75c.tar.gz
spack-df44045fdb68e429bd1accdb7e7c9b947521d75c.tar.bz2
spack-df44045fdb68e429bd1accdb7e7c9b947521d75c.tar.xz
spack-df44045fdb68e429bd1accdb7e7c9b947521d75c.zip
Feature: use git branches/tags as versions (#31200)
Building on #24639, this allows versions to be prefixed by `git.`. If a version begins `git.`, it is treated as a git ref, and handled as git commits are starting in the referenced PR. An exception is made for versions that are `git.develop`, `git.main`, `git.master`, `git.head`, or `git.trunk`. Those are assumed to be greater than all other versions, as those prefixed strings are in other contexts.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/cmd/checksum.py4
-rw-r--r--lib/spack/spack/directives.py14
-rw-r--r--lib/spack/spack/fetch_strategy.py22
-rw-r--r--lib/spack/spack/package_base.py6
-rw-r--r--lib/spack/spack/solver/asp.py15
-rw-r--r--lib/spack/spack/spec.py4
-rw-r--r--lib/spack/spack/test/versions.py62
-rw-r--r--lib/spack/spack/version.py297
8 files changed, 304 insertions, 120 deletions
diff --git a/lib/spack/spack/cmd/checksum.py b/lib/spack/spack/cmd/checksum.py
index f88417dd2f..37c06ac7bb 100644
--- a/lib/spack/spack/cmd/checksum.py
+++ b/lib/spack/spack/cmd/checksum.py
@@ -16,7 +16,7 @@ import spack.stage
import spack.util.crypto
from spack.package_base import preferred_version
from spack.util.naming import valid_fully_qualified_module_name
-from spack.version import Version, ver
+from spack.version import VersionBase, ver
description = "checksum available versions of a package"
section = "packaging"
@@ -65,7 +65,7 @@ def checksum(parser, args):
remote_versions = None
for version in versions:
version = ver(version)
- if not isinstance(version, Version):
+ if not isinstance(version, VersionBase):
tty.die("Cannot generate checksums for version lists or "
"version ranges. Use unambiguous versions.")
url = pkg.find_valid_url_for_version(version)
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index 2de6552a82..0801db6146 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -46,7 +46,7 @@ import spack.variant
from spack.dependency import Dependency, canonical_deptype, default_deptype
from spack.fetch_strategy import from_kwargs
from spack.resource import Resource
-from spack.version import Version, VersionChecksumError
+from spack.version import GitVersion, Version, VersionChecksumError, VersionLookupError
__all__ = ['DirectiveError', 'DirectiveMeta', 'version', 'conflicts', 'depends_on',
'extends', 'provides', 'patch', 'variant', 'resource']
@@ -330,7 +330,17 @@ def version(ver, checksum=None, **kwargs):
kwargs['checksum'] = checksum
# Store kwargs for the package to later with a fetch_strategy.
- pkg.versions[Version(ver)] = kwargs
+ version = Version(ver)
+ if isinstance(version, GitVersion):
+ if not hasattr(pkg, 'git') and 'git' not in kwargs:
+ msg = "Spack version directives cannot include git hashes fetched from"
+ msg += " URLs. Error in package '%s'\n" % pkg.name
+ msg += " version('%s', " % version.string
+ msg += ', '.join("%s='%s'" % (argname, value)
+ for argname, value in kwargs.items())
+ msg += ")"
+ raise VersionLookupError(msg)
+ pkg.versions[version] = kwargs
return _execute_version
diff --git a/lib/spack/spack/fetch_strategy.py b/lib/spack/spack/fetch_strategy.py
index 9f907c5da6..ed2e5eeb42 100644
--- a/lib/spack/spack/fetch_strategy.py
+++ b/lib/spack/spack/fetch_strategy.py
@@ -1575,16 +1575,30 @@ def for_package_version(pkg, version):
check_pkg_attributes(pkg)
- if not isinstance(version, spack.version.Version):
+ if not isinstance(version, spack.version.VersionBase):
version = spack.version.Version(version)
# if it's a commit, we must use a GitFetchStrategy
- if version.is_commit and hasattr(pkg, "git"):
+ if isinstance(version, spack.version.GitVersion):
+ if not hasattr(pkg, "git"):
+ raise FetchError(
+ "Cannot fetch git version for %s. Package has no 'git' attribute" %
+ pkg.name
+ )
# Populate the version with comparisons to other commits
- version.generate_commit_lookup(pkg.name)
+ version.generate_git_lookup(pkg.name)
+
+ # For GitVersion, we have no way to determine whether a ref is a branch or tag
+ # Fortunately, we handle branches and tags identically, except tags are
+ # handled slightly more conservatively for older versions of git.
+ # We call all non-commit refs tags in this context, at the cost of a slight
+ # performance hit for branches on older versions of git.
+ # Branches cannot be cached, so we tell the fetcher not to cache tags/branches
+ ref_type = 'commit' if version.is_commit else 'tag'
kwargs = {
'git': pkg.git,
- 'commit': str(version)
+ ref_type: version.ref,
+ 'no_cache': True,
}
kwargs['submodules'] = getattr(pkg, 'submodules', False)
fetcher = GitFetchStrategy(**kwargs)
diff --git a/lib/spack/spack/package_base.py b/lib/spack/spack/package_base.py
index 9153310bd2..b0c272ed30 100644
--- a/lib/spack/spack/package_base.py
+++ b/lib/spack/spack/package_base.py
@@ -62,7 +62,7 @@ from spack.stage import ResourceStage, Stage, StageComposite, stage_prefix
from spack.util.executable import ProcessError, which
from spack.util.package_hash import package_hash
from spack.util.prefix import Prefix
-from spack.version import Version
+from spack.version import GitVersion, Version, VersionBase
if sys.version_info[0] >= 3:
FLAG_HANDLER_RETURN_TYPE = Tuple[
@@ -1041,7 +1041,7 @@ class PackageBase(six.with_metaclass(PackageMeta, PackageViewMixin, object)):
return self._implement_all_urls_for_version(version, uf)
def _implement_all_urls_for_version(self, version, custom_url_for_version=None):
- if not isinstance(version, Version):
+ if not isinstance(version, VersionBase):
version = Version(version)
urls = []
@@ -1505,7 +1505,7 @@ class PackageBase(six.with_metaclass(PackageMeta, PackageViewMixin, object)):
checksum = spack.config.get('config:checksum')
fetch = self.stage.managed_by_spack
if checksum and fetch and (self.version not in self.versions) \
- and (not self.version.is_commit):
+ and (not isinstance(self.version, GitVersion)):
tty.warn("There is no checksum on file to fetch %s safely." %
self.spec.cformat('{name}{@version}'))
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 38c03008e2..5a466ab78a 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -1429,11 +1429,16 @@ class SpackSolverSetup(object):
continue
known_versions = self.possible_versions[dep.name]
- if (not dep.version.is_commit and
+ if (not isinstance(dep.version, spack.version.GitVersion) and
any(v.satisfies(dep.version) for v in known_versions)):
# some version we know about satisfies this constraint, so we
# should use that one. e.g, if the user asks for qt@5 and we
- # know about qt@5.5.
+ # know about qt@5.5. This ensures we don't add under-specified
+ # versions to the solver
+ #
+ # For git versions, we know the version is already fully specified
+ # so we don't have to worry about whether it's an under-specified
+ # version
continue
# if there is a concrete version on the CLI *that we know nothing
@@ -1678,7 +1683,7 @@ class SpackSolverSetup(object):
# extract all the real versions mentioned in version ranges
def versions_for(v):
- if isinstance(v, spack.version.Version):
+ if isinstance(v, spack.version.VersionBase):
return [v]
elif isinstance(v, spack.version.VersionRange):
result = [v.start] if v.start else []
@@ -2187,8 +2192,8 @@ class SpecBuilder(object):
# concretization process)
for root in self._specs.values():
for spec in root.traverse():
- if spec.version.is_commit:
- spec.version.generate_commit_lookup(spec.fullname)
+ if isinstance(spec.version, spack.version.GitVersion):
+ spec.version.generate_git_lookup(spec.fullname)
return self._specs
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 602dcb09e8..7ebb34894d 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -5154,9 +5154,9 @@ class SpecParser(spack.parse.Parser):
# Note: VersionRange(x, x) is currently concrete, hence isinstance(...).
if (
spec.name and spec.versions.concrete and
- isinstance(spec.version, vn.Version) and spec.version.is_commit
+ isinstance(spec.version, vn.GitVersion)
):
- spec.version.generate_commit_lookup(spec.fullname)
+ spec.version.generate_git_lookup(spec.fullname)
return specs
diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py
index c8e7e45196..bcfebcd264 100644
--- a/lib/spack/spack/test/versions.py
+++ b/lib/spack/spack/test/versions.py
@@ -17,7 +17,14 @@ from llnl.util.filesystem import working_dir
import spack.package_base
import spack.spec
from spack.util.executable import which
-from spack.version import Version, VersionList, VersionRange, ver
+from spack.version import (
+ GitVersion,
+ Version,
+ VersionBase,
+ VersionList,
+ VersionRange,
+ ver,
+)
def assert_ver_lt(a, b):
@@ -520,7 +527,7 @@ def test_repr_and_str():
def check_repr_and_str(vrs):
a = Version(vrs)
- assert repr(a) == "Version('" + vrs + "')"
+ assert repr(a) == "VersionBase('" + vrs + "')"
b = eval(repr(a))
assert a == b
assert str(a) == vrs
@@ -544,19 +551,19 @@ def test_get_item():
assert isinstance(a[1], int)
# Test slicing
b = a[0:2]
- assert isinstance(b, Version)
+ assert isinstance(b, VersionBase)
assert b == Version('0.1')
- assert repr(b) == "Version('0.1')"
+ assert repr(b) == "VersionBase('0.1')"
assert str(b) == '0.1'
b = a[0:3]
- assert isinstance(b, Version)
+ assert isinstance(b, VersionBase)
assert b == Version('0.1_2')
- assert repr(b) == "Version('0.1_2')"
+ assert repr(b) == "VersionBase('0.1_2')"
assert str(b) == '0.1_2'
b = a[1:]
- assert isinstance(b, Version)
+ assert isinstance(b, VersionBase)
assert b == Version('1_2-3')
- assert repr(b) == "Version('1_2-3')"
+ assert repr(b) == "VersionBase('1_2-3')"
assert str(b) == '1_2-3'
# Raise TypeError on tuples
with pytest.raises(TypeError):
@@ -597,7 +604,7 @@ def test_versions_from_git(mock_git_version_info, monkeypatch, mock_packages):
spec = spack.spec.Spec('git-test-commit@%s' % commit)
version = spec.version
comparator = [str(v) if not isinstance(v, int) else v
- for v in version._cmp(version.commit_lookup)]
+ for v in version._cmp(version.ref_lookup)]
with working_dir(repo_path):
which('git')('checkout', commit)
@@ -637,6 +644,43 @@ def test_git_hash_comparisons(
assert spec4.satisfies('@1.0:1.2')
+@pytest.mark.skipif(sys.platform == 'win32',
+ reason="Not supported on Windows (yet)")
+def test_git_ref_comparisons(
+ mock_git_version_info, install_mockery, mock_packages, monkeypatch):
+ """Check that hashes compare properly to versions
+ """
+ repo_path, filename, commits = mock_git_version_info
+ monkeypatch.setattr(spack.package_base.PackageBase,
+ 'git', 'file://%s' % repo_path,
+ raising=False)
+
+ # Spec based on tag v1.0
+ spec_tag = spack.spec.Spec('git-test-commit@git.v1.0')
+ spec_tag.concretize()
+ assert spec_tag.satisfies('@1.0')
+ assert not spec_tag.satisfies('@1.1:')
+ assert str(spec_tag.version) == 'git.v1.0'
+
+ # Spec based on branch 1.x
+ spec_branch = spack.spec.Spec('git-test-commit@git.1.x')
+ spec_branch.concretize()
+ assert spec_branch.satisfies('@1.2')
+ assert spec_branch.satisfies('@1.1:1.3')
+ assert str(spec_branch.version) == 'git.1.x'
+
+
+@pytest.mark.parametrize('string,git', [
+ ('1.2.9', False),
+ ('gitmain', False),
+ ('git.foo', True),
+ ('git.abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd', True),
+ ('abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd', True),
+])
+def test_version_git_vs_base(string, git):
+ assert isinstance(Version(string), GitVersion) == git
+
+
def test_version_range_nonempty():
assert Version('1.2.9') in VersionRange('1.2.0', '1.2')
assert Version('1.1.1') in ver('1.0:1')
diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py
index 5fa7793da2..4267d307b1 100644
--- a/lib/spack/spack/version.py
+++ b/lib/spack/spack/version.py
@@ -67,11 +67,11 @@ iv_min_len = min(len(s) for s in infinity_versions)
def coerce_versions(a, b):
"""
Convert both a and b to the 'greatest' type between them, in this order:
- Version < VersionRange < VersionList
+ VersionBase < GitVersion < VersionRange < VersionList
This is used to simplify comparison operations below so that we're always
comparing things that are of the same type.
"""
- order = (Version, VersionRange, VersionList)
+ order = (VersionBase, GitVersion, VersionRange, VersionList)
ta, tb = type(a), type(b)
def check_type(t):
@@ -83,12 +83,16 @@ def coerce_versions(a, b):
if ta == tb:
return (a, b)
elif order.index(ta) > order.index(tb):
- if ta == VersionRange:
+ if ta == GitVersion:
+ return (a, GitVersion(b))
+ elif ta == VersionRange:
return (a, VersionRange(b, b))
else:
return (a, VersionList([b]))
else:
- if tb == VersionRange:
+ if tb == GitVersion:
+ return (GitVersion(a), b)
+ elif tb == VersionRange:
return (VersionRange(a, a), b)
else:
return (VersionList([a]), b)
@@ -165,15 +169,29 @@ class VersionStrComponent(object):
return not self.__lt__(other)
-class Version(object):
+def is_git_version(string):
+ if string.startswith('git.'):
+ return True
+ elif len(string) == 40 and COMMIT_VERSION.match(string):
+ return True
+ return False
+
+
+def Version(string): # capitalized for backwards compatibility
+ if not isinstance(string, str):
+ string = str(string) # to handle VersionBase and GitVersion types
+
+ if is_git_version(string):
+ return GitVersion(string)
+ return VersionBase(string)
+
+
+class VersionBase(object):
"""Class to represent versions"""
__slots__ = [
"version",
"separators",
"string",
- "_commit_lookup",
- "is_commit",
- "commit_version",
]
def __init__(self, string):
@@ -188,36 +206,12 @@ class Version(object):
if string and not VALID_VERSION.match(string):
raise ValueError("Bad characters in version string: %s" % string)
- # An object that can lookup git commits to compare them to versions
- self._commit_lookup = None
- self.commit_version = None
segments = SEGMENT_REGEX.findall(string)
self.version = tuple(
int(m[0]) if m[0] else VersionStrComponent(m[1]) for m in segments
)
self.separators = tuple(m[2] for m in segments)
- self.is_commit = len(self.string) == 40 and COMMIT_VERSION.match(self.string)
-
- def _cmp(self, other_lookups=None):
- commit_lookup = self.commit_lookup or other_lookups
-
- if self.is_commit and commit_lookup:
- if self.commit_version is not None:
- return self.commit_version
- commit_info = commit_lookup.get(self.string)
- if commit_info:
- prev_version, distance = commit_info
-
- # Extend previous version by empty component and distance
- # If commit is exactly a known version, no distance suffix
- prev_tuple = Version(prev_version).version if prev_version else ()
- dist_suffix = (VersionStrComponent(''), distance) if distance else ()
- self.commit_version = prev_tuple + dist_suffix
- return self.commit_version
-
- return self.version
-
@property
def dotted(self):
"""The dotted representation of the version.
@@ -230,7 +224,7 @@ class Version(object):
Returns:
Version: The version with separator characters replaced by dots
"""
- return Version(self.string.replace('-', '.').replace('_', '.'))
+ return type(self)(self.string.replace('-', '.').replace('_', '.'))
@property
def underscored(self):
@@ -245,7 +239,7 @@ class Version(object):
Version: The version with separator characters replaced by
underscores
"""
- return Version(self.string.replace('.', '_').replace('-', '_'))
+ return type(self)(self.string.replace('.', '_').replace('-', '_'))
@property
def dashed(self):
@@ -259,7 +253,7 @@ class Version(object):
Returns:
Version: The version with separator characters replaced by dashes
"""
- return Version(self.string.replace('.', '-').replace('_', '-'))
+ return type(self)(self.string.replace('.', '-').replace('_', '-'))
@property
def joined(self):
@@ -273,7 +267,7 @@ class Version(object):
Returns:
Version: The version with separator characters removed
"""
- return Version(
+ return type(self)(
self.string.replace('.', '').replace('-', '').replace('_', ''))
def up_to(self, index):
@@ -323,13 +317,9 @@ class Version(object):
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
a suitable compiler.
"""
- self_cmp = self._cmp(other.commit_lookup)
- other_cmp = other._cmp(self.commit_lookup)
-
- # Do the final comparison
- nself = len(self_cmp)
- nother = len(other_cmp)
- return nother <= nself and self_cmp[:nother] == other_cmp
+ nself = len(self.version)
+ nother = len(other.version)
+ return nother <= nself and self.version[:nother] == other.version
def __iter__(self):
return iter(self.version)
@@ -356,19 +346,19 @@ class Version(object):
string_arg = ''.join(string_arg)
return cls(string_arg)
else:
- return Version('')
+ return VersionBase('')
message = '{cls.__name__} indices must be integers'
raise TypeError(message.format(cls=cls))
def __repr__(self):
- return 'Version(' + repr(self.string) + ')'
+ return 'VersionBase(' + repr(self.string) + ')'
def __str__(self):
return self.string
def __format__(self, format_spec):
- return self.string.format(format_spec)
+ return str(self).format(format_spec)
@property
def concrete(self):
@@ -384,22 +374,16 @@ class Version(object):
if other is None:
return False
- # If either is a commit and we haven't indexed yet, can't compare
- if (other.is_commit or self.is_commit) and not (self.commit_lookup or
- other.commit_lookup):
- return False
-
# Use tuple comparison assisted by VersionStrComponent for performance
- return self._cmp(other.commit_lookup) < other._cmp(self.commit_lookup)
+ return self.version < other.version
@coerced
def __eq__(self, other):
-
# Cut out early if we don't have a version
- if other is None or type(other) != Version:
+ if other is None or type(other) != VersionBase:
return False
- return self._cmp(other.commit_lookup) == other._cmp(self.commit_lookup)
+ return self.version == other.version
@coerced
def __ne__(self, other):
@@ -425,24 +409,23 @@ class Version(object):
if other is None:
return False
- self_cmp = self._cmp(other.commit_lookup)
- return other._cmp(self.commit_lookup)[:len(self_cmp)] == self_cmp
+ return other.version[:len(self.version)] == self.version
+ @coerced
def is_predecessor(self, other):
"""True if the other version is the immediate predecessor of this one.
- That is, NO non-commit versions v exist such that:
+ That is, NO non-git versions v exist such that:
(self < v < other and v not in self).
"""
- self_cmp = self._cmp(self.commit_lookup)
- other_cmp = other._cmp(other.commit_lookup)
-
- if self_cmp[:-1] != other_cmp[:-1]:
+ if self.version[:-1] != other.version[:-1]:
return False
- sl = self_cmp[-1]
- ol = other_cmp[-1]
+ sl = self.version[-1]
+ ol = other.version[-1]
+ # TODO: extend this to consecutive letters, z/0, and infinity versions
return type(sl) == int and type(ol) == int and (ol - sl == 1)
+ @coerced
def is_successor(self, other):
return other.is_predecessor(self)
@@ -468,13 +451,135 @@ class Version(object):
else:
return VersionList()
+
+class GitVersion(VersionBase):
+ """Class to represent versions interpreted from git refs.
+
+ Non-git versions may be coerced to GitVersion for comparison, but no Spec will ever
+ have a GitVersion that is not actually referencing a version from git."""
+ def __init__(self, string):
+ if not isinstance(string, str):
+ string = str(string) # In case we got a VersionBase or GitVersion object
+
+ git_prefix = string.startswith('git.')
+ self.ref = string[4:] if git_prefix else string
+
+ self.is_commit = len(self.ref) == 40 and COMMIT_VERSION.match(self.ref)
+ self.is_ref = git_prefix # is_ref False only for comparing to VersionBase
+ self.is_ref |= bool(self.is_commit)
+
+ # ensure git.<hash> and <hash> are treated the same by dropping 'git.'
+ canonical_string = self.ref if self.is_commit else string
+ super(GitVersion, self).__init__(canonical_string)
+
+ # An object that can lookup git refs to compare them to versions
+ self._ref_lookup = None
+ self.ref_version = None
+
+ def _cmp(self, other_lookups=None):
+ # No need to rely on git comparisons for develop-like refs
+ if len(self.version) == 2 and self.isdevelop():
+ return self.version
+
+ # If we've already looked this version up, return cached value
+ if self.ref_version is not None:
+ return self.ref_version
+
+ ref_lookup = self.ref_lookup or other_lookups
+
+ if self.is_ref and ref_lookup:
+ ref_info = ref_lookup.get(self.ref)
+ if ref_info:
+ prev_version, distance = ref_info
+
+ # Extend previous version by empty component and distance
+ # If commit is exactly a known version, no distance suffix
+ prev_tuple = VersionBase(prev_version).version if prev_version else ()
+ dist_suffix = (VersionStrComponent(''), distance) if distance else ()
+ self.ref_version = prev_tuple + dist_suffix
+ return self.ref_version
+
+ return self.version
+
+ @coerced
+ def satisfies(self, other):
+ """A Version 'satisfies' another if it is at least as specific and has
+ a common prefix. e.g., we want gcc@4.7.3 to satisfy a request for
+ gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
+ a suitable compiler.
+ """
+ self_cmp = self._cmp(other.ref_lookup)
+ other_cmp = other._cmp(self.ref_lookup)
+
+ # Do the final comparison
+ nself = len(self_cmp)
+ nother = len(other_cmp)
+ return nother <= nself and self_cmp[:nother] == other_cmp
+
+ def __repr__(self):
+ return 'GitVersion(' + repr(self.string) + ')'
+
+ @coerced
+ def __lt__(self, other):
+ """Version comparison is designed for consistency with the way RPM
+ does things. If you need more complicated versions in installed
+ packages, you should override your package's version string to
+ express it more sensibly.
+ """
+ if other is None:
+ return False
+
+ # If we haven't indexed yet, can't compare
+ # If we called this, we know at least one is a git ref
+ if not (self.ref_lookup or other.ref_lookup):
+ return False
+
+ # Use tuple comparison assisted by VersionStrComponent for performance
+ return self._cmp(other.ref_lookup) < other._cmp(self.ref_lookup)
+
+ @coerced
+ def __eq__(self, other):
+ # Cut out early if we don't have a git version
+ if other is None or type(other) != GitVersion:
+ return False
+
+ return self._cmp(other.ref_lookup) == other._cmp(self.ref_lookup)
+
+ def __hash__(self):
+ return hash(str(self))
+
+ @coerced
+ def __contains__(self, other):
+ if other is None:
+ return False
+
+ self_cmp = self._cmp(other.ref_lookup)
+ return other._cmp(self.ref_lookup)[:len(self_cmp)] == self_cmp
+
+ @coerced
+ def is_predecessor(self, other):
+ """True if the other version is the immediate predecessor of this one.
+ That is, NO non-commit versions v exist such that:
+ (self < v < other and v not in self).
+ """
+ self_cmp = self._cmp(self.ref_lookup)
+ other_cmp = other._cmp(other.ref_lookup)
+
+ if self_cmp[:-1] != other_cmp[:-1]:
+ return False
+
+ sl = self_cmp[-1]
+ ol = other_cmp[-1]
+ return type(sl) == int and type(ol) == int and (ol - sl == 1)
+
@property
- def commit_lookup(self):
- if self._commit_lookup:
- self._commit_lookup.get(self.string)
- return self._commit_lookup
+ def ref_lookup(self):
+ if self._ref_lookup:
+ # Get operation ensures dict is populated
+ self._ref_lookup.get(self.ref)
+ return self._ref_lookup
- def generate_commit_lookup(self, pkg_name):
+ def generate_git_lookup(self, pkg_name):
"""
Use the git fetcher to look up a version for a commit.
@@ -492,11 +597,11 @@ class Version(object):
"""
# Sanity check we have a commit
- if not self.is_commit:
- tty.die("%s is not a commit." % self)
+ if not self.is_ref:
+ tty.die("%s is not a git version." % self)
# Generate a commit looker-upper
- self._commit_lookup = CommitLookup(pkg_name)
+ self._ref_lookup = CommitLookup(pkg_name)
class VersionRange(object):
@@ -715,7 +820,7 @@ class VersionList(object):
self.add(ver(v))
def add(self, version):
- if type(version) in (Version, VersionRange):
+ if type(version) in (VersionBase, GitVersion, VersionRange):
# This normalizes single-value version ranges.
if version.concrete:
version = version.concrete
@@ -968,7 +1073,7 @@ def ver(obj):
return _string_to_version(obj)
elif isinstance(obj, (int, float)):
return _string_to_version(str(obj))
- elif type(obj) in (Version, VersionRange, VersionList):
+ elif type(obj) in (VersionBase, GitVersion, VersionRange, VersionList):
return obj
else:
raise TypeError("ver() can't convert %s to version!" % type(obj))
@@ -990,8 +1095,8 @@ class CommitLookup(object):
"""An object for cached lookups of git commits
CommitLookup objects delegate to the misc_cache for locking.
- CommitLookup objects may be attached to a Version object for which
- Version.is_commit returns True to allow for comparisons between git commits
+ CommitLookup objects may be attached to a GitVersion object for which
+ Version.is_ref returns True to allow for comparisons between git refs
and versions as represented by tags in the git repository.
"""
def __init__(self, pkg_name):
@@ -1074,17 +1179,17 @@ class CommitLookup(object):
with spack.caches.misc_cache.read_transaction(self.cache_key) as cache_file:
self.data = sjson.load(cache_file)
- def get(self, commit):
+ def get(self, ref):
if not self.data:
self.load_data()
- if commit not in self.data:
- self.data[commit] = self.lookup_commit(commit)
+ if ref not in self.data:
+ self.data[ref] = self.lookup_ref(ref)
self.save()
- return self.data[commit]
+ return self.data[ref]
- def lookup_commit(self, commit):
+ def lookup_ref(self, ref):
"""Lookup the previous version and distance for a given commit.
We use git to compare the known versions from package to the git tags,
@@ -1111,13 +1216,19 @@ class CommitLookup(object):
# remote instance, simply adding '-f' may not be sufficient
# (if commits are deleted on the remote, this command alone
# won't properly update the local rev-list)
- self.fetcher.git("fetch", '--tags')
+ self.fetcher.git("fetch", '--tags', output=os.devnull, error=os.devnull)
- # Ensure commit is an object known to git
- # Note the brackets are literals, the commit replaces the format string
- # This will raise a ProcessError if the commit does not exist
- # We may later design a custom error to re-raise
- self.fetcher.git('cat-file', '-e', '%s^{commit}' % commit)
+ # Ensure ref is a commit object known to git
+ # Note the brackets are literals, the ref replaces the format string
+ try:
+ self.fetcher.git(
+ 'cat-file', '-e', '%s^{commit}' % ref,
+ output=os.devnull, error=os.devnull
+ )
+ except spack.util.executable.ProcessError:
+ raise VersionLookupError(
+ "%s is not a valid git ref for %s" % (ref, self.pkg_name)
+ )
# List tags (refs) by date, so last reference of a tag is newest
tag_info = self.fetcher.git(
@@ -1148,11 +1259,11 @@ class CommitLookup(object):
ancestor_commits = []
for tag_commit in commit_to_version:
self.fetcher.git(
- 'merge-base', '--is-ancestor', tag_commit, commit,
+ 'merge-base', '--is-ancestor', tag_commit, ref,
ignore_errors=[1])
if self.fetcher.git.returncode == 0:
distance = self.fetcher.git(
- 'rev-list', '%s..%s' % (tag_commit, commit), '--count',
+ 'rev-list', '%s..%s' % (tag_commit, ref), '--count',
output=str, error=str).strip()
ancestor_commits.append((tag_commit, int(distance)))
@@ -1164,14 +1275,14 @@ class CommitLookup(object):
else:
# Get list of all commits, this is in reverse order
# We use this to get the first commit below
- commit_info = self.fetcher.git("log", "--all", "--pretty=format:%H",
- output=str)
- commits = [c for c in commit_info.split('\n') if c]
+ ref_info = self.fetcher.git("log", "--all", "--pretty=format:%H",
+ output=str)
+ commits = [c for c in ref_info.split('\n') if c]
# No previous version and distance from first commit
prev_version = None
distance = int(self.fetcher.git(
- 'rev-list', '%s..%s' % (commits[-1], commit), '--count',
+ 'rev-list', '%s..%s' % (commits[-1], ref), '--count',
output=str, error=str
).strip())