From 78ddf9120326f9a617d95c07a68883294edab2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Dunand?= Date: Mon, 25 Jun 2012 16:07:41 +0200 Subject: [PATCH] Handle complex version number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Image version now accept a string of digit separated by dots and extra char as a qualifier (~ or +). Example: 1.2.3.4~dev < 1.2.3.4 < 1.2.3.4+dev Old repositories are still compatible thanks to sqlite type affinity (https://www.sqlite.org/faq.html#q3). We use a package version comparison algorithm for image version comparison inspired by Debian. Seblu: Fix typos Signed-off-by: Sébastien Luttringer --- README | 12 +++-- bin/is | 2 +- installsystems/image.py | 6 +-- installsystems/repository.py | 11 +++-- installsystems/template.py | 2 +- installsystems/tools.py | 88 +++++++++++++++++++++++++++++------- 6 files changed, 91 insertions(+), 30 deletions(-) diff --git a/README b/README index 5bc67f1..9799c19 100644 --- a/README +++ b/README @@ -4,8 +4,8 @@ INSTALLSYSTEMS VERSIONNING __________________________ A valid version is an integer without dot. -A version n, may be followed by a ~, to indicate it's inferior to n -A version n, may be followed by a +, to indicate it's superior to n +A version n may be followed by a ~ to indicate it's inferior to n +A version n may be followed by a + to indicate it's superior to n Any following chars after ~ or + are ignored Examples: @@ -17,4 +17,10 @@ Examples: IMAGES VERSIONNING __________________ -A valid version is an integer. Nothing more! \ No newline at end of file +A valid version is a string of digits separated by dots. +A version n may be followed by a ~ to indicate it's inferior to n +A version n may be followed by a + to indicate it's superior to n + +Examples: + 1.2.3~dev < 1.2.3 < 1.2.3+dev + 7~dev < 7 < 7+dev diff --git a/bin/is b/bin/is index b384713..10d0237 100755 --- a/bin/is +++ b/bin/is @@ -160,7 +160,7 @@ def c_changelog(args): ''' repoman = load_repositories(args) for image, repo in get_images(args.pattern, repoman, min=1): - image.changelog.show(int(image.version), args.all_version) + image.changelog.show(image.version, args.all_version) def c_check(args): ''' diff --git a/installsystems/image.py b/installsystems/image.py index fcfe5ab..ab91cf2 100644 --- a/installsystems/image.py +++ b/installsystems/image.py @@ -69,7 +69,7 @@ class Image(object): ''' Check if @buf is a valid image version ''' - if re.match("^\d+$", buf) is None: + if re.match("^\d+(\.\d+)*(([~+]).*)?$", buf) is None: raise ISError(u"Invalid image version %s" % buf) @staticmethod @@ -1340,9 +1340,9 @@ class Changelog(dict): if line.lstrip().startswith("#"): continue # try to match a new version - m = re.match("\[(\d+)\]", line.lstrip()) + m = re.match("\[(\d+(?:\.\d+)*)(?:([~+]).*)?\]", line.lstrip()) if m is not None: - version = int(m.group(1)) + version = m.group(1) self[version] = [] continue # if line are out of a version => invalid format diff --git a/installsystems/repository.py b/installsystems/repository.py index 6edadcf..4ae93da 100644 --- a/installsystems/repository.py +++ b/installsystems/repository.py @@ -196,14 +196,15 @@ class Repository(object): def last(self, name): ''' - Return last version of name in repo or -1 if not found + Return last version of name in repo or None if not found ''' - r = self.db.ask("SELECT version FROM image WHERE name = ? ORDER BY version DESC LIMIT 1", (name,)).fetchone() + r = self.db.ask("SELECT version FROM image WHERE name = ?", (name,)).fetchall() # no row => no way if r is None: - return -1 + return None + f = lambda x,y: x[0] if istools.compare_versions(x[0], y[0]) > 0 else y[0] # return last - return r[0] + return reduce(f, r) def add(self, image, delete=False): ''' @@ -455,7 +456,7 @@ class Repository(object): # is no version take the last if version is None: version = self.last(name) - if version < 0: + if version is None: raise ISError(u"Unable to find image %s in %s" % (name, self.config.name)) # get file md5 from db diff --git a/installsystems/template.py b/installsystems/template.py index 778db20..3643f06 100644 --- a/installsystems/template.py +++ b/installsystems/template.py @@ -82,7 +82,7 @@ arrow(u"hostname: %s" % namespace.hostname) createdb = u""" CREATE TABLE image (md5 TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, - version INTEGER NOT NULL, + version TEXT NOT NULL, date INTEGER NOT NULL, author TEXT, description TEXT, diff --git a/installsystems/tools.py b/installsystems/tools.py index b997f06..9d2e884 100644 --- a/installsystems/tools.py +++ b/installsystems/tools.py @@ -589,28 +589,82 @@ def compare_versions(v1, v2): return > 0 if v1 > v2 return < 0 if v2 > v1 return = 0 if v1 == v2 + + This uses the Debian package version sorting algorithm (see 'man deb-version') ''' - def get_ver(version): - '''Return float version''' - if type(version) is int or type(version) is float: - return float(version) - elif isinstance(version, basestring): - iv = re.match("^(\d+)(?:([-~+]).*)?$", version) - if iv is None: - raise TypeError(u"Invalid version format: %s" % version) - rv = float(iv.group(1)) - if iv.group(2) == "~": - rv -= 0.1 + # Ensure versions have the right format + for version in v1, v2: + iv = re.match("^(\d+(?:\.\d+)*)(?:([~+]).*)?$", str(version)) + if iv is None: + raise TypeError(u"Invalid version format: %s" % version) + + digitregex = re.compile(r'^([0-9]*)(.*)$') + nondigitregex = re.compile(r'^([^0-9]*)(.*)$') + + digits = True + while v1 or v2: + pattern = digitregex if digits else nondigitregex + sub_v1, v1 = pattern.findall(str(v1))[0] + sub_v2, v2 = pattern.findall(str(v2))[0] + + if digits: + sub_v1 = int(sub_v1 if sub_v1 else 0) + sub_v2 = int(sub_v2 if sub_v2 else 0) + if sub_v1 < sub_v2: + rv = -1 + elif sub_v1 > sub_v2: + rv = 1 else: - rv += 0.1 - return rv + rv = 0 + if rv != 0: + return rv else: - raise TypeError(u"Invalid version format: %s" % version) + rv = strvercmp(sub_v1, sub_v2) + if rv != 0: + return rv + + digits = not digits + return 0 + +def strvercmp(lhs, rhs): + ''' + Compare string part of a version number + ''' + size = max(len(lhs), len(rhs)) + lhs_array = str_version_array(lhs, size) + rhs_array = str_version_array(rhs, size) + if lhs_array > rhs_array: + return 1 + elif lhs_array < rhs_array: + return -1 + else: + return 0 + +def str_version_array(str_version, size): + ''' + Turns a string into an array of numeric values kind-of corresponding to + the ASCII numeric values of the characters in the string. I say 'kind-of' + because any character which is not an alphabetic character will be + it's ASCII value + 256, and the tilde (~) character will have the value + -1. - fv1 = get_ver(v1) - fv2 = get_ver(v2) - return fv1 - fv2 + Additionally, the +size+ parameter specifies how long the array needs to + be; any elements in the array beyond the length of the string will be 0. + + This method has massive ASCII assumptions. Use with caution. + ''' + a = [0] * size + for i, char in enumerate(str_version): + char = ord(char) + if ((char >= ord('a') and char <= ord('z')) or + (char >= ord('A') and char <= ord('Z'))): + a[i] = char + elif char == ord('~'): + a[i] = -1 + else: + a[i] = char + 256 + return a def get_compressor_path(name, compress=True, level=None): ''' -- GitLab