// 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'; } } }