| # 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] |