| // Copyright 2020 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| import 'dart:io'; | |
| import 'package:args/command_runner.dart'; | |
| import '../utils.dart'; | |
| // All other devtools_* pubspecs have their own versioning strategies, or do not | |
| // have a version at all (in the case of devtools_test). | |
| final _devtoolsAppPubspec = File( | |
| pathFromRepoRoot('packages/devtools_app/pubspec.yaml'), | |
| ); | |
| final _releaseNoteDirPath = pathFromRepoRoot( | |
| 'packages/devtools_app/release_notes', | |
| ); | |
| class UpdateDevToolsVersionCommand extends Command { | |
| UpdateDevToolsVersionCommand() { | |
| addSubcommand(ManualUpdateCommand()); | |
| addSubcommand(AutoUpdateCommand()); | |
| addSubcommand(CurrentVersionCommand()); | |
| } | |
| @override | |
| String get name => 'update-version'; | |
| @override | |
| String get description => | |
| 'Updates the main DevTools version and any packages that are versioned ' | |
| 'in lock-step.'; | |
| } | |
| Future<void> performTheVersionUpdate({ | |
| required String currentVersion, | |
| required String newVersion, | |
| }) async { | |
| print( | |
| 'Updating devtools_app/pubspec.yaml from $currentVersion to version ' | |
| '$newVersion...', | |
| ); | |
| writeVersionToPubspec(_devtoolsAppPubspec, newVersion); | |
| print('Updating devtools.dart to version $newVersion...'); | |
| writeVersionToVersionFile( | |
| File(pathFromRepoRoot('packages/devtools_app/lib/devtools.dart')), | |
| newVersion, | |
| ); | |
| } | |
| Future<void> resetReleaseNotes({required String version}) async { | |
| print('Resetting the release notes'); | |
| // Clear out the current notes | |
| final imagesDir = Directory('$_releaseNoteDirPath/images'); | |
| if (imagesDir.existsSync()) { | |
| await imagesDir.delete(recursive: true); | |
| } | |
| await imagesDir.create(); | |
| await File('$_releaseNoteDirPath/images/.gitkeep').create(); | |
| final currentReleaseNotesFile = File( | |
| '$_releaseNoteDirPath/NEXT_RELEASE_NOTES.md', | |
| ); | |
| if (currentReleaseNotesFile.existsSync()) { | |
| await currentReleaseNotesFile.delete(); | |
| } | |
| // Normalize the version number so that it onl | |
| final semVerMatch = RegExp( | |
| r'^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)', | |
| ).firstMatch(version); | |
| if (semVerMatch == null) { | |
| throw 'Version format is unexpected'; | |
| } | |
| final major = int.parse(semVerMatch.namedGroup('major')!, radix: 10); | |
| final minor = int.parse(semVerMatch.namedGroup('minor')!, radix: 10); | |
| final normalizedVersionNumber = '$major.$minor.0'; | |
| final templateFile = File( | |
| '$_releaseNoteDirPath/helpers/release_notes_template.md', | |
| ); | |
| final templateFileContents = await templateFile.readAsString(); | |
| await currentReleaseNotesFile.writeAsString( | |
| templateFileContents.replaceAll( | |
| RegExp(r'<number>'), | |
| normalizedVersionNumber, | |
| ), | |
| ); | |
| } | |
| String? incrementVersionByType(String version, String type) { | |
| final semVerMatch = RegExp( | |
| r'^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)', | |
| ).firstMatch(version); | |
| if (semVerMatch == null) { | |
| throw 'Version format is unexpected'; | |
| } | |
| var major = int.parse(semVerMatch.namedGroup('major')!, radix: 10); | |
| var minor = int.parse(semVerMatch.namedGroup('minor')!, radix: 10); | |
| var patch = int.parse(semVerMatch.namedGroup('patch')!, radix: 10); | |
| switch (type) { | |
| case 'major': | |
| major++; | |
| minor = 0; | |
| patch = 0; | |
| break; | |
| case 'minor': | |
| minor++; | |
| patch = 0; | |
| break; | |
| case 'patch': | |
| patch++; | |
| break; | |
| default: | |
| return null; | |
| } | |
| return '$major.$minor.$patch'; | |
| } | |
| String? versionFromPubspecFile() { | |
| final lines = _devtoolsAppPubspec.readAsLinesSync(); | |
| for (final line in lines) { | |
| if (line.startsWith(pubspecVersionPrefix)) { | |
| return line.substring(pubspecVersionPrefix.length).trim(); | |
| } | |
| } | |
| return null; | |
| } | |
| void writeVersionToPubspec(File pubspec, String version) { | |
| final lines = pubspec.readAsLinesSync(); | |
| final revisedLines = <String>[]; | |
| String? currentSection = ''; | |
| final sectionRegExp = RegExp('([a-z]|_)+:'); | |
| for (var line in lines) { | |
| if (line.startsWith(sectionRegExp)) { | |
| // This is a top level section of the pubspec. | |
| currentSection = sectionRegExp.firstMatch(line)![0]; | |
| } | |
| if (currentSection == pubspecVersionPrefix && | |
| line.startsWith(pubspecVersionPrefix)) { | |
| line = [ | |
| line.substring( | |
| 0, | |
| line.indexOf(pubspecVersionPrefix) + pubspecVersionPrefix.length, | |
| ), | |
| ' $version', | |
| ].join(); | |
| } | |
| revisedLines.add(line); | |
| } | |
| final content = revisedLines.joinWithNewLine(); | |
| pubspec.writeAsStringSync(content); | |
| } | |
| void writeVersionToVersionFile(File versionFile, String version) { | |
| const prefix = 'const version = '; | |
| final lines = versionFile.readAsLinesSync(); | |
| final revisedLines = <String>[]; | |
| for (var line in lines) { | |
| if (line.startsWith(prefix)) { | |
| line = [prefix, '\'$version\';'].join(); | |
| } | |
| revisedLines.add(line); | |
| } | |
| versionFile.writeAsStringSync(revisedLines.joinWithNewLine()); | |
| } | |
| String incrementDevVersion(String currentVersion) { | |
| final alreadyHasDevVersion = isDevVersion(currentVersion); | |
| if (alreadyHasDevVersion) { | |
| final devVerMatch = RegExp( | |
| r'^(?<prefix>\d+\.\d+\.\d+.*-dev\.)(?<devVersion>\d+)(?<suffix>.*)$', | |
| ).firstMatch(currentVersion); | |
| if (devVerMatch == null) { | |
| throw 'Invalid version, could not increment dev version'; | |
| } else { | |
| final prefix = devVerMatch.namedGroup('prefix')!; | |
| final devVersion = devVerMatch.namedGroup('devVersion')!; | |
| final suffix = devVerMatch.namedGroup('suffix')!; | |
| final bumpedDevVersion = int.parse(devVersion, radix: 10) + 1; | |
| final newVersion = '$prefix$bumpedDevVersion$suffix'; | |
| return newVersion; | |
| } | |
| } else { | |
| return '$currentVersion-dev.0'; | |
| } | |
| } | |
| String stripPreReleases(String currentVersion) { | |
| final devVerMatch = RegExp( | |
| r'^(?<semver>\d+\.\d+\.\d+).*$', | |
| ).firstMatch(currentVersion); | |
| if (devVerMatch == null) { | |
| throw 'Could not strip pre-releases from version: $currentVersion'; | |
| } else { | |
| return devVerMatch.namedGroup('semver')!; | |
| } | |
| } | |
| bool isDevVersion(String version) { | |
| return RegExp(r'-dev\.\d+').hasMatch(version); | |
| } | |
| String calculateNewVersion(String currentVersion, String type) { | |
| switch (type) { | |
| case 'release': | |
| if (isDevVersion(currentVersion)) { | |
| return stripPreReleases(currentVersion); | |
| } else { | |
| return incrementVersionByType(currentVersion, 'minor')!; | |
| } | |
| case 'dev': | |
| return incrementDevVersion(currentVersion); | |
| default: | |
| final version = incrementVersionByType(currentVersion, type); | |
| if (version == null) { | |
| throw 'Failed to determine the newVersion.'; | |
| } | |
| return version; | |
| } | |
| } | |
| const pubspecVersionPrefix = 'version:'; | |
| class ManualUpdateCommand extends Command { | |
| ManualUpdateCommand() { | |
| argParser | |
| ..addOption( | |
| 'new-version', | |
| abbr: 'n', | |
| mandatory: true, | |
| help: 'The new version code that devtools will be set to.', | |
| ) | |
| ..addOption( | |
| 'current-version', | |
| abbr: 'c', | |
| help: '''The current devtools version, this should be set to the version | |
| inside the index.html. This is only necessary to set this if automatic | |
| detection is failing.''', | |
| ); | |
| } | |
| @override | |
| final name = 'manual'; | |
| @override | |
| final description = 'Manually update devtools to a new version.'; | |
| @override | |
| Future<void> run() async { | |
| final newVersion = argResults!['new-version'] as String; | |
| final currentVersion = | |
| (argResults!['current-version'] as String?) ?? versionFromPubspecFile(); | |
| if (currentVersion == null) { | |
| throw 'Could not determine the version, please set the current-version or determine why getting the version is failing.'; | |
| } | |
| await performTheVersionUpdate( | |
| currentVersion: currentVersion, | |
| newVersion: newVersion, | |
| ); | |
| } | |
| } | |
| class CurrentVersionCommand extends Command { | |
| @override | |
| final name = 'current-version'; | |
| @override | |
| final description = 'Print the current devtools_app version.'; | |
| @override | |
| void run() async { | |
| print(versionFromPubspecFile()); | |
| } | |
| } | |
| class AutoUpdateCommand extends Command { | |
| AutoUpdateCommand() { | |
| argParser | |
| ..addOption( | |
| 'type', | |
| abbr: 't', | |
| allowed: ['release', 'dev', 'patch', 'minor', 'major'], | |
| allowedHelp: { | |
| 'release': [ | |
| 'strips any pre-release versions from the version.', | |
| 'Examples:', | |
| '\t1.2.3 => 1.2.3', | |
| '\t1.2.3-dev.4 => 1.2.3', | |
| ].join('\n'), | |
| 'dev': [ | |
| 'bumps the version to the next dev pre-release value (minor by default).', | |
| 'Examples:', | |
| '\t1.2.3 => 1.2.3-dev.0', | |
| '\t1.2.3-dev.4 => 1.2.3-dev.5', | |
| ].join('\n'), | |
| 'patch': [ | |
| 'bumps the version to the next patch value.', | |
| 'Examples:', | |
| '\t1.2.3 => 1.2.4', | |
| '\t1.2.3-dev.4 => 1.2.4', | |
| ].join('\n'), | |
| 'minor': [ | |
| 'bumps the version to the next minor value.', | |
| 'Examples:', | |
| '\t1.2.3 => 1.3.0', | |
| '\t1.2.3-dev.4 => 1.3.0', | |
| ].join('\n'), | |
| 'major': [ | |
| 'bumps the version to the next major value.', | |
| 'Examples:', | |
| '\t1.2.3 => 2.0.0', | |
| '\t1.2.3-dev.4 => 2.0.0', | |
| ].join('\n'), | |
| }, | |
| mandatory: true, | |
| help: 'Bumps the devtools version by the selected type.', | |
| ) | |
| ..addFlag( | |
| 'dry-run', | |
| abbr: 'd', | |
| defaultsTo: false, | |
| help: | |
| 'Displays the version change that would happen, without performing ' | |
| 'it.', | |
| ); | |
| } | |
| @override | |
| final name = 'auto'; | |
| @override | |
| final description = 'Automatically update devtools to a new version.'; | |
| @override | |
| Future<void> run() async { | |
| final type = argResults!['type'] as String; | |
| final isDryRun = argResults!['dry-run'] as bool; | |
| final currentVersion = versionFromPubspecFile(); | |
| if (currentVersion == null) { | |
| throw 'Could not automatically determine current version.'; | |
| } | |
| final newVersion = calculateNewVersion(currentVersion, type); | |
| print('Bump version from $currentVersion to $newVersion'); | |
| if (isDryRun) { | |
| return; | |
| } | |
| await performTheVersionUpdate( | |
| currentVersion: currentVersion, | |
| newVersion: newVersion, | |
| ); | |
| if (['minor', 'major'].contains(type)) { | |
| // Only cycle the release notes when doing a minor or major version bump | |
| await resetReleaseNotes(version: newVersion); | |
| } | |
| } | |
| } |