blob: f6a30eb02f4a16d37b386cebb9c273b28568cef4 [file] [log] [blame]
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
from functools import total_ordering
from recipe_engine import recipe_api
@total_ordering
class ReleaseVersion(object):
"""
Representation of a release version, versioned by major number > date >
release number > candidate number.
ReleaseVersions are guaranteed to be valid release versions.
An AssertionError will be raised if an invalid release version would be
created upon initialization or modification.
"""
_TEMPLATE = 'releases/{major}.{date}.{release}.{candidate}'
_MAJOR_PATTERN = '[0-9]'
_DATE_PATTERN = '[0-9]{8}'
_RELEASE_PATTERN = '[0-9]{1,10}'
_CANDIDATE_PATTERN = '[1-9][0-9]{0,9}'
_MAJOR_REGEXP = re.compile('^%s$' % _MAJOR_PATTERN)
_DATE_REGEXP = re.compile('^%s$' % _DATE_PATTERN)
_RELEASE_REGEXP = re.compile('^%s$' % _RELEASE_PATTERN)
_CANDIDATE_REGEXP = re.compile('^%s$' % _CANDIDATE_PATTERN)
def __init__(self, major, date, release, candidate):
self.major = major
self.date = date
self.release = release
self.candidate = candidate
@property
def major(self):
return self._major
@major.setter
def major(self, value):
"""
Set major number.
Must be a single digit.
"""
value = int(value)
assert re.match(self._MAJOR_REGEXP, str(value)), \
'%s does not match regexp %s' % (value, self._MAJOR_REGEXP.pattern)
self._major = value
@property
def date(self):
return self._date
@date.setter
def date(self, value):
"""
Set date.
Must be in YYYYMMDD format.
"""
value = str(value)
assert re.match(self._DATE_REGEXP, value), \
'%s does not match regexp %s' % (value, self._DATE_REGEXP.pattern)
self._date = value
@property
def release(self):
return self._release
@release.setter
def release(self, value):
"""
Set release number.
Must be 1-10 digits long.
"""
value = int(value)
assert re.match(self._RELEASE_REGEXP, str(value)), \
'%s does not match regexp %s' % (value, self._RELEASE_REGEXP.pattern)
self._release = value
@property
def candidate(self):
return self._candidate
@candidate.setter
def candidate(self, value):
"""
Set release number.
Must be 1-10 digits long, and greater than 0.
"""
value = int(value)
assert re.match(self._CANDIDATE_REGEXP, str(value)), \
'%s does not match regexp %s' % (value, self._CANDIDATE_REGEXP.pattern)
self._candidate = value
@classmethod
def get_regexp(cls,
major=_MAJOR_PATTERN,
date=_DATE_PATTERN,
release=_RELEASE_PATTERN,
candidate=_CANDIDATE_PATTERN):
"""
Create a release version regexp.
Default values can be overriden to restrict regexp matching.
"""
return cls._TEMPLATE.format(
major='(?P<major>%s)' % major,
date='(?P<date>%s)' % date,
release='(?P<release>%s)' % release,
candidate='(?P<candidate>%s)' % candidate)
@classmethod
def from_string(cls, string):
"""
Create a release version from a string.
"""
match = re.search(cls.get_regexp(), string)
assert match
return cls(
major=match.group('major'),
date=match.group('date'),
release=match.group('release'),
candidate=match.group('candidate'))
def __repr__(self):
"""
Return a string representation of this release version.
"""
return self._TEMPLATE.format(
major=self.major,
date=self.date,
release=self.release,
candidate=self.candidate)
def __eq__(self, other):
"""
Check whether this release version is equal to another.
"""
return ((self.major, self.date, self.release, self.candidate) ==
(other.major, other.date, other.release, other.candidate)) # yapf: disable
def __lt__(self, other):
"""
Check whether this release version is less than another.
A release version is less than another by this precedence:
major number < date < release number < candidate number
"""
return ((self.major, self.date, self.release, self.candidate) <
(other.major, other.date, other.release, other.candidate))
class ReleaseApi(recipe_api.RecipeApi):
"""
The release module provides utilities for release commit and version
generation on a git repository.
"""
_COMMIT_MESSAGE_PREFIX = '[release]'
_REVISION_LENGTH = 10
_BUILDSET_TEMPLATE = 'commit/gitiles/{stripped_https_remote}/+/{revision}'
# Always set major number to 0 by default.
_MAJOR_NUMBER = 0
def __init__(self, *args, **kwargs):
super(ReleaseApi, self).__init__(*args, **kwargs)
def get_initial_release_version(self, date):
"""
Format an initial release version. An initial release version only
depends on the input date.
Args:
date (str): Release date as YYYYMMDD.
Returns:
ReleaseVersion: Initial release version.
"""
return ReleaseVersion(
major=self._MAJOR_NUMBER, date=date, release=0, candidate=1)
def get_next_release_version(self, ref, date, repo_path):
"""
Get next release version for a ref and date.
If there is a release version reachable on ref for the input date, the
next release version is the max existing release version reachable on ref
with its release number incremented by 1, and candidate number reset to 1.
Otherwise, the next release version is an initial release version for
the input date.
Args:
ref (str): Target git ref.
date (str): Release date as YYYYMMDD.
repo_path (Path): Local git repository to query release versions.
Returns:
ReleaseVersion: Next release version.
"""
release_versions = self.get_release_versions(
ref=ref, date=date, repo_path=repo_path)
release_version = self.get_initial_release_version(date=date)
if release_versions:
latest_release_version = max(release_versions)
# If the latest release version on branch matches input date, increment
# release number by 1.
if latest_release_version.date == date:
release_version.release = latest_release_version.release + 1
return release_version
def get_next_candidate_version(self, ref, repo_path, add_track=False):
"""
Get next candidate version for a ref.
The next candidate version is the maximum release version reachable on ref
with its candidate version incremented by 1. Note that a candidate cannot
be created on a revision without at least one existing release version in
the ref's history.
Args:
ref (str): Target git ref.
repo_path (Path): Local git repository to query release versions.
add_track (bool): Whether to start a new track.
Returns:
ReleaseVersion: Next candidate version.
Raises:
StepFailure: No release versions found in ref's history.
"""
release_versions = self.get_release_versions(ref=ref, repo_path=repo_path)
# There must be existing release versions in ref's history.
if not release_versions:
raise self.m.step.StepFailure('No release versions found')
# Take the latest release version and increment its candidate version.
release_version = max(release_versions)
if add_track:
next_candidate = int(str(release_version.candidate) + '001')
else:
next_candidate = release_version.candidate + 1
release_version.candidate = next_candidate
return release_version
def get_release_versions(self,
ref,
repo_path,
date=ReleaseVersion._DATE_PATTERN):
"""
Get release versions for a ref and date.
Args:
ref (str): Target git ref.
date (str): Release date as YYYYMMDD. Accepts pattern.
repo_path (Path): Local git repository to query release versions.
Returns:
list<ReleaseVersion>: Matching release versions.
"""
with self.m.step.nest('get release versions on {ref}'.format(ref=ref)):
with self.m.context(cwd=repo_path):
tags = self.m.git(
'--no-pager', 'tag', '--merged', ref,
stdout=self.m.raw_io.output()).stdout
matches = re.findall(
ReleaseVersion.get_regexp(date=date), tags, re.MULTILINE)
return [
ReleaseVersion(
major=major, date=date, release=release, candidate=candidate)
for major, date, release, candidate in matches
]
def ref_to_release_version(self, ref, repo_path):
"""
Convert a ref to a release version.
Args:
ref (str): Target git ref.
repo_path (Path): Local git repository to describe ref in.
Raises:
StepFailure: input ref is not a git tag, or it is a tag but not a
valid release version.
"""
with self.m.context(cwd=repo_path):
tag = self.m.git.describe(commit=ref, expected_num=1)[0]
try:
release_version = ReleaseVersion.from_string(tag)
except AssertionError:
raise self.m.step.StepFailure('Tag %s is not a release version' % tag)
return release_version
def shorten_revision(self, revision):
"""
Shorten a revision.
Args:
revision (str): A git revision SHA1.
Returns:
str: Shortened revision.
"""
return revision[:ReleaseApi._REVISION_LENGTH]