| // Copyright 2014 The Flutter 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 'globals.dart' show ConductorException, releaseCandidateBranchRegex; |
| |
| import 'proto/conductor_state.pbenum.dart'; |
| |
| /// Possible string formats that `flutter --version` can return. |
| enum VersionType { |
| /// A stable flutter release. |
| /// |
| /// Example: '1.2.3' |
| stable, |
| |
| /// A pre-stable flutter release. |
| /// |
| /// Example: '1.2.3-4.5.pre' |
| development, |
| |
| /// A master channel flutter version. |
| /// |
| /// Example: '1.2.3-4.0.pre.10' |
| /// |
| /// The last number is the number of commits past the last tagged version. |
| latest, |
| |
| /// A master channel flutter version from git describe. |
| /// |
| /// Example: '1.2.3-4.0.pre-10-gabc123'. |
| /// Example: '1.2.3-10-gabc123'. |
| gitDescribe, |
| } |
| |
| final Map<VersionType, RegExp> versionPatterns = <VersionType, RegExp>{ |
| VersionType.stable: RegExp(r'^(\d+)\.(\d+)\.(\d+)$'), |
| VersionType.development: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'), |
| VersionType.latest: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre\.(\d+)$'), |
| VersionType.gitDescribe: RegExp(r'^(\d+)\.(\d+)\.(\d+)-((\d+)\.(\d+)\.pre-)?(\d+)-g[a-f0-9]+$'), |
| }; |
| |
| class Version { |
| Version({ |
| required this.x, |
| required this.y, |
| required this.z, |
| this.m, |
| this.n, |
| this.commits, |
| required this.type, |
| }) { |
| switch (type) { |
| case VersionType.stable: |
| assert(m == null); |
| assert(n == null); |
| assert(commits == null); |
| break; |
| case VersionType.development: |
| assert(m != null); |
| assert(n != null); |
| assert(commits == null); |
| break; |
| case VersionType.latest: |
| assert(m != null); |
| assert(n != null); |
| assert(commits != null); |
| break; |
| case VersionType.gitDescribe: |
| assert(commits != null); |
| break; |
| } |
| } |
| |
| /// Create a new [Version] from a version string. |
| /// |
| /// It is expected that [versionString] will be generated by |
| /// `flutter --version` and match one of `stablePattern`, `developmentPattern` |
| /// and `latestPattern`. |
| factory Version.fromString(String versionString) { |
| assert(versionString != null); |
| |
| versionString = versionString.trim(); |
| // stable tag |
| Match? match = versionPatterns[VersionType.stable]!.firstMatch(versionString); |
| if (match != null) { |
| // parse stable |
| final List<int> parts = match |
| .groups(<int>[1, 2, 3]) |
| .map((String? s) => int.parse(s!)) |
| .toList(); |
| return Version( |
| x: parts[0], |
| y: parts[1], |
| z: parts[2], |
| type: VersionType.stable, |
| ); |
| } |
| // development tag |
| match = versionPatterns[VersionType.development]!.firstMatch(versionString); |
| if (match != null) { |
| // parse development |
| final List<int> parts = |
| match.groups(<int>[1, 2, 3, 4, 5]).map((String? s) => int.parse(s!)).toList(); |
| return Version( |
| x: parts[0], |
| y: parts[1], |
| z: parts[2], |
| m: parts[3], |
| n: parts[4], |
| type: VersionType.development, |
| ); |
| } |
| // latest tag |
| match = versionPatterns[VersionType.latest]!.firstMatch(versionString); |
| if (match != null) { |
| // parse latest |
| final List<int> parts = match.groups( |
| <int>[1, 2, 3, 4, 5, 6], |
| ).map( |
| (String? s) => int.parse(s!), |
| ).toList(); |
| return Version( |
| x: parts[0], |
| y: parts[1], |
| z: parts[2], |
| m: parts[3], |
| n: parts[4], |
| commits: parts[5], |
| type: VersionType.latest, |
| ); |
| } |
| match = versionPatterns[VersionType.gitDescribe]!.firstMatch(versionString); |
| if (match != null) { |
| // parse latest |
| final int x = int.parse(match.group(1)!); |
| final int y = int.parse(match.group(2)!); |
| final int z = int.parse(match.group(3)!); |
| final int? m = int.tryParse(match.group(5) ?? ''); |
| final int? n = int.tryParse(match.group(6) ?? ''); |
| final int commits = int.parse(match.group(7)!); |
| return Version( |
| x: x, |
| y: y, |
| z: z, |
| m: m, |
| n: n, |
| commits: commits, |
| type: VersionType.gitDescribe, |
| ); |
| } |
| throw Exception('${versionString.trim()} cannot be parsed'); |
| } |
| |
| // Returns a new version with the given [increment] part incremented. |
| // NOTE new version must be of same type as previousVersion. |
| factory Version.increment( |
| Version previousVersion, |
| String increment, { |
| VersionType? nextVersionType, |
| }) { |
| final int nextX = previousVersion.x; |
| int nextY = previousVersion.y; |
| int nextZ = previousVersion.z; |
| int? nextM = previousVersion.m; |
| int? nextN = previousVersion.n; |
| if (nextVersionType == null) { |
| if (previousVersion.type == VersionType.latest || previousVersion.type == VersionType.gitDescribe) { |
| nextVersionType = VersionType.development; |
| } else { |
| nextVersionType = previousVersion.type; |
| } |
| } |
| |
| switch (increment) { |
| case 'x': |
| // This was probably a mistake. |
| throw Exception('Incrementing x is not supported by this tool.'); |
| case 'y': |
| // Dev release following a beta release. |
| nextY += 1; |
| nextZ = 0; |
| if (previousVersion.type != VersionType.stable) { |
| nextM = 0; |
| nextN = 0; |
| } |
| break; |
| case 'z': |
| // Hotfix to stable release. |
| assert(previousVersion.type == VersionType.stable); |
| nextZ += 1; |
| break; |
| case 'm': |
| assert(false, "Do not increment 'm' via Version.increment, use instead Version.fromCandidateBranch()"); |
| break; |
| case 'n': |
| // Hotfix to internal roll. |
| nextN = nextN! + 1; |
| break; |
| default: |
| throw Exception('Unknown increment level $increment.'); |
| } |
| return Version( |
| x: nextX, |
| y: nextY, |
| z: nextZ, |
| m: nextM, |
| n: nextN, |
| type: nextVersionType, |
| ); |
| } |
| |
| factory Version.fromCandidateBranch(String branchName) { |
| // Regular dev release. |
| final RegExp pattern = RegExp(r'flutter-(\d+)\.(\d+)-candidate.(\d+)'); |
| final RegExpMatch? match = pattern.firstMatch(branchName); |
| late final int x; |
| late final int y; |
| late final int m; |
| try { |
| x = int.parse(match!.group(1)!); |
| y = int.parse(match.group(2)!); |
| m = int.parse(match.group(3)!); |
| } on Exception { |
| throw ConductorException('branch named $branchName not recognized as a valid candidate branch'); |
| } |
| |
| return Version( |
| type: VersionType.development, |
| x: x, |
| y: y, |
| z: 0, |
| m: m, |
| n: 0, |
| ); |
| } |
| |
| /// Major version. |
| final int x; |
| |
| /// Zero-indexed count of beta releases after a major release. |
| final int y; |
| |
| /// Number of hotfix releases after a stable release. |
| /// |
| /// For non-stable releases, this will be 0. |
| final int z; |
| |
| /// Zero-indexed count of dev releases after a beta release. |
| /// |
| /// For stable releases, this will be null. |
| final int? m; |
| |
| /// Number of hotfixes required to make a dev release. |
| /// |
| /// For stable releases, this will be null. |
| final int? n; |
| |
| /// Number of commits past last tagged dev release. |
| final int? commits; |
| |
| final VersionType type; |
| |
| /// Validate that the parsed version is valid. |
| /// |
| /// Will throw a [ConductorException] if the version is not possible given the |
| /// [candidateBranch] and [incrementLetter]. |
| void ensureValid(String candidateBranch, ReleaseType releaseType) { |
| final RegExpMatch? branchMatch = releaseCandidateBranchRegex.firstMatch(candidateBranch); |
| if (branchMatch == null) { |
| throw ConductorException( |
| 'Candidate branch $candidateBranch does not match the pattern ' |
| '${releaseCandidateBranchRegex.pattern}', |
| ); |
| } |
| |
| // These groups are required in the pattern, so these match groups should |
| // not be null |
| final String branchX = branchMatch.group(1)!; |
| if (x != int.tryParse(branchX)) { |
| throw ConductorException( |
| 'Parsed version $this has a different x value than candidate ' |
| 'branch $candidateBranch', |
| ); |
| } |
| final String branchY = branchMatch.group(2)!; |
| if (y != int.tryParse(branchY)) { |
| throw ConductorException( |
| 'Parsed version $this has a different y value than candidate ' |
| 'branch $candidateBranch', |
| ); |
| } |
| |
| // stable type versions don't have an m field set |
| if (type != VersionType.stable && releaseType != ReleaseType.STABLE_HOTFIX && releaseType != ReleaseType.STABLE_INITIAL) { |
| final String branchM = branchMatch.group(3)!; |
| if (m != int.tryParse(branchM)) { |
| throw ConductorException( |
| 'Parsed version $this has a different m value than candidate ' |
| 'branch $candidateBranch with type $type', |
| ); |
| } |
| } |
| } |
| |
| @override |
| String toString() { |
| switch (type) { |
| case VersionType.stable: |
| return '$x.$y.$z'; |
| case VersionType.development: |
| return '$x.$y.$z-$m.$n.pre'; |
| case VersionType.latest: |
| return '$x.$y.$z-$m.$n.pre.$commits'; |
| case VersionType.gitDescribe: |
| return '$x.$y.$z-$m.$n.pre.$commits'; |
| } |
| } |
| } |