blob: 716d6e8c116a9b6e1e50050ff7cab020be9cf5fe [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
import attr
from recipe_engine import recipe_api
# Regex patterns for each part of a release version.
MAJOR_PATTERN = r"[0-9]"
DATE_PATTERN = r"[0-9]{8}"
RELEASE_PATTERN = r"[0-9]{1,10}"
CANDIDATE_PATTERN = r"[1-9][0-9]{0,9}"
def int_match(pattern):
"""Constructs an attrs validator asserting that an int matches the pattern.
Conforms to the attrs validator API, raising an exception if the
given value does not match the validator's pattern.
"""
regex = re.compile(r"^%s$" % pattern)
def validator(_, attribute, value):
match = regex.match(str(value))
assert match, "%r value %r does not match regex %r" % (
attribute.name,
value,
pattern,
)
return validator
@attr.s(eq=True, order=True) # Set up __eq__() and __lt__() based on attribs.
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.
"""
# Note that the order of the attrib declarations is also the order in which
# they are compared when doing less-than/greater-than comparisons. So it's
# important that they be defined in order of "significance", most significant
# first.
major = attr.ib(converter=int, validator=int_match(MAJOR_PATTERN))
date = attr.ib(converter=int, validator=int_match(DATE_PATTERN))
release = attr.ib(converter=int, validator=int_match(RELEASE_PATTERN))
candidate = attr.ib(converter=int, validator=int_match(CANDIDATE_PATTERN))
_TEMPLATE = "releases/{major}.{date}.{release}.{candidate}"
def __str__(self):
return self._TEMPLATE.format(
major=self.major,
date=self.date,
release=self.release,
candidate=self.candidate,
)
@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 overridden to restrict regexp matching.
"""
return cls._TEMPLATE.replace(".", "\.").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"),
)
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 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 == int(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 ref's release version with its candidate
version incremented by 1.
Args:
ref (str): Target git ref.
repo_path (Path): Local git repository to resolve release version.
add_track (bool): Whether to start a new track.
Returns:
ReleaseVersion: Next candidate version.
Raises:
StepFailure: input ref is not a git tag, or it is a tag but not a
valid release version.
"""
release_version = self.ref_to_release_version(ref=ref, repo_path=repo_path)
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=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.context(cwd=repo_path):
tags = self.m.git(
"get release versions on %s" % ref,
"--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]