[flutter_conductor] update dev/tools with release tool (#69791)
This commit is contained in:
parent
1f210275aa
commit
772437627b
1
dev/tools/.gitignore
vendored
Normal file
1
dev/tools/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
checkouts/
|
39
dev/tools/bin/conductor
Executable file
39
dev/tools/bin/conductor
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Needed because if it is set, cd may print the path it changed to.
|
||||
unset CDPATH
|
||||
|
||||
# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one
|
||||
# link at a time, and then cds into the link destination and find out where it
|
||||
# ends up.
|
||||
#
|
||||
# The returned filesystem path must be a format usable by Dart's URI parser,
|
||||
# since the Dart command line tool treats its argument as a file URI, not a
|
||||
# filename. For instance, multiple consecutive slashes should be reduced to a
|
||||
# single slash, since double-slashes indicate a URI "authority", and these are
|
||||
# supposed to be filenames. There is an edge case where this will return
|
||||
# multiple slashes: when the input resolves to the root directory. However, if
|
||||
# that were the case, we wouldn't be running this shell, so we don't do anything
|
||||
# about it.
|
||||
#
|
||||
# The function is enclosed in a subshell to avoid changing the working directory
|
||||
# of the caller.
|
||||
function follow_links() (
|
||||
cd -P "$(dirname -- "$1")"
|
||||
file="$PWD/$(basename -- "$1")"
|
||||
while [[ -h "$file" ]]; do
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$(readlink -- "$file")"
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$PWD/$(basename -- "$file")"
|
||||
done
|
||||
echo "$file"
|
||||
)
|
||||
|
||||
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
|
||||
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
|
||||
DART_BIN="$BIN_DIR/../../../bin/dart"
|
||||
|
||||
"$DART_BIN" --enable-asserts "$BIN_DIR/conductor.dart" "$@"
|
77
dev/tools/bin/conductor.dart
Normal file
77
dev/tools/bin/conductor.dart
Normal file
@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
// Rolls the dev channel.
|
||||
// Only tested on Linux.
|
||||
//
|
||||
// See: https://github.com/flutter/flutter/wiki/Release-process
|
||||
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:dev_tools/repository.dart';
|
||||
import 'package:dev_tools/roll_dev.dart';
|
||||
import 'package:dev_tools/stdio.dart';
|
||||
|
||||
void main(List<String> args) {
|
||||
const FileSystem fileSystem = LocalFileSystem();
|
||||
const ProcessManager processManager = LocalProcessManager();
|
||||
const Platform platform = LocalPlatform();
|
||||
final Stdio stdio = VerboseStdio(
|
||||
stdout: io.stdout,
|
||||
stderr: io.stderr,
|
||||
stdin: io.stdin,
|
||||
);
|
||||
final Checkouts checkouts = Checkouts(
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
);
|
||||
final CommandRunner<void> runner = CommandRunner<void>(
|
||||
'conductor',
|
||||
'A tool for coordinating Flutter releases.',
|
||||
usageLineLength: 80,
|
||||
);
|
||||
|
||||
<Command<void>>[
|
||||
RollDev(
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
repository: checkouts.addRepo(
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
repoType: RepositoryType.framework,
|
||||
stdio: stdio,
|
||||
),
|
||||
stdio: stdio,
|
||||
),
|
||||
].forEach(runner.addCommand);
|
||||
|
||||
if (!assertsEnabled()) {
|
||||
stdio.printError('The conductor tool must be run with --enable-asserts.');
|
||||
io.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
runner.run(args);
|
||||
} on Exception catch (e) {
|
||||
stdio.printError(e.toString());
|
||||
io.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool assertsEnabled() {
|
||||
// Verify asserts enabled
|
||||
bool assertsEnabled = false;
|
||||
|
||||
assert(() {
|
||||
assertsEnabled = true;
|
||||
return true;
|
||||
}());
|
||||
return assertsEnabled;
|
||||
}
|
72
dev/tools/lib/git.dart
Normal file
72
dev/tools/lib/git.dart
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import './globals.dart';
|
||||
|
||||
/// A wrapper around git process calls that can be mocked for unit testing.
|
||||
class Git {
|
||||
Git(this.processManager) : assert(processManager != null);
|
||||
|
||||
final ProcessManager processManager;
|
||||
|
||||
String getOutput(
|
||||
List<String> args,
|
||||
String explanation, {
|
||||
@required String workingDirectory,
|
||||
}) {
|
||||
final ProcessResult result = _run(args, workingDirectory);
|
||||
if (result.exitCode == 0) {
|
||||
return stdoutToString(result.stdout);
|
||||
}
|
||||
_reportFailureAndExit(args, workingDirectory, result, explanation);
|
||||
return null; // for the analyzer's sake
|
||||
}
|
||||
|
||||
int run(
|
||||
List<String> args,
|
||||
String explanation, {
|
||||
bool allowNonZeroExitCode = false,
|
||||
@required String workingDirectory,
|
||||
}) {
|
||||
final ProcessResult result = _run(args, workingDirectory);
|
||||
if (result.exitCode != 0 && !allowNonZeroExitCode) {
|
||||
_reportFailureAndExit(args, workingDirectory, result, explanation);
|
||||
}
|
||||
return result.exitCode;
|
||||
}
|
||||
|
||||
ProcessResult _run(List<String> args, String workingDirectory) {
|
||||
return processManager.runSync(
|
||||
<String>['git', ...args],
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
}
|
||||
|
||||
void _reportFailureAndExit(
|
||||
List<String> args,
|
||||
String workingDirectory,
|
||||
ProcessResult result,
|
||||
String explanation,
|
||||
) {
|
||||
final StringBuffer message = StringBuffer();
|
||||
if (result.exitCode != 0) {
|
||||
message.writeln(
|
||||
'Command "git ${args.join(' ')}" failed in directory "$workingDirectory" to '
|
||||
'$explanation. Git exited with error code ${result.exitCode}.',
|
||||
);
|
||||
} else {
|
||||
message.writeln('Command "git ${args.join(' ')}" failed to $explanation.');
|
||||
}
|
||||
if ((result.stdout as String).isNotEmpty)
|
||||
message.writeln('stdout from git:\n${result.stdout}\n');
|
||||
if ((result.stderr as String).isNotEmpty)
|
||||
message.writeln('stderr from git:\n${result.stderr}\n');
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
26
dev/tools/lib/globals.dart
Normal file
26
dev/tools/lib/globals.dart
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
const String kIncrement = 'increment';
|
||||
const String kCommit = 'commit';
|
||||
const String kRemoteName = 'remote';
|
||||
const String kJustPrint = 'just-print';
|
||||
const String kYes = 'yes';
|
||||
const String kForce = 'force';
|
||||
const String kSkipTagging = 'skip-tagging';
|
||||
|
||||
const String kUpstreamRemote = 'https://github.com/flutter/flutter.git';
|
||||
|
||||
const List<String> kReleaseChannels = <String>[
|
||||
'stable',
|
||||
'beta',
|
||||
'dev',
|
||||
'master',
|
||||
];
|
||||
|
||||
/// Cast a dynamic to String and trim.
|
||||
String stdoutToString(dynamic input) {
|
||||
final String str = input as String;
|
||||
return str.trim();
|
||||
}
|
365
dev/tools/lib/repository.dart
Normal file
365
dev/tools/lib/repository.dart
Normal file
@ -0,0 +1,365 @@
|
||||
// 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 'dart:convert' show jsonDecode;
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
import './git.dart';
|
||||
import './globals.dart' as globals;
|
||||
import './stdio.dart';
|
||||
import './version.dart';
|
||||
|
||||
/// A source code repository.
|
||||
class Repository {
|
||||
Repository({
|
||||
@required this.name,
|
||||
@required this.upstream,
|
||||
@required this.processManager,
|
||||
@required this.stdio,
|
||||
@required this.platform,
|
||||
@required this.fileSystem,
|
||||
@required this.parentDirectory,
|
||||
this.localUpstream = false,
|
||||
this.useExistingCheckout = false,
|
||||
}) : git = Git(processManager),
|
||||
assert(localUpstream != null),
|
||||
assert(useExistingCheckout != null);
|
||||
|
||||
final String name;
|
||||
final String upstream;
|
||||
final Git git;
|
||||
final ProcessManager processManager;
|
||||
final Stdio stdio;
|
||||
final Platform platform;
|
||||
final FileSystem fileSystem;
|
||||
final Directory parentDirectory;
|
||||
final bool useExistingCheckout;
|
||||
|
||||
/// If the repository will be used as an upstream for a test repo.
|
||||
final bool localUpstream;
|
||||
|
||||
Directory _checkoutDirectory;
|
||||
|
||||
/// Lazily-loaded directory for the repository checkout.
|
||||
///
|
||||
/// Cloning a repository is time-consuming, thus the repository is not cloned
|
||||
/// until this getter is called.
|
||||
Directory get checkoutDirectory {
|
||||
if (_checkoutDirectory != null) {
|
||||
return _checkoutDirectory;
|
||||
}
|
||||
_checkoutDirectory = parentDirectory.childDirectory(name);
|
||||
if (checkoutDirectory.existsSync() && !useExistingCheckout) {
|
||||
deleteDirectory();
|
||||
}
|
||||
if (!checkoutDirectory.existsSync()) {
|
||||
stdio.printTrace('Cloning $name to ${checkoutDirectory.path}...');
|
||||
git.run(
|
||||
<String>['clone', '--', upstream, checkoutDirectory.path],
|
||||
'Cloning $name repo',
|
||||
workingDirectory: parentDirectory.path,
|
||||
);
|
||||
if (localUpstream) {
|
||||
// These branches must exist locally for the repo that depends on it
|
||||
// to fetch and push to.
|
||||
for (final String channel in globals.kReleaseChannels) {
|
||||
git.run(
|
||||
<String>['checkout', channel, '--'],
|
||||
'check out branch $channel locally',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stdio.printTrace(
|
||||
'Using existing $name repo at ${checkoutDirectory.path}...',
|
||||
);
|
||||
}
|
||||
return _checkoutDirectory;
|
||||
}
|
||||
|
||||
void deleteDirectory() {
|
||||
if (!checkoutDirectory.existsSync()) {
|
||||
stdio.printTrace(
|
||||
'Tried to delete ${checkoutDirectory.path} but it does not exist.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
stdio.printTrace('Deleting $name from ${checkoutDirectory.path}...');
|
||||
checkoutDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
/// The URL of the remote named [remoteName].
|
||||
String remoteUrl(String remoteName) {
|
||||
assert(remoteName != null);
|
||||
return git.getOutput(
|
||||
<String>['remote', 'get-url', remoteName],
|
||||
'verify the URL of the $remoteName remote',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify the repository's git checkout is clean.
|
||||
bool gitCheckoutClean() {
|
||||
final String output = git.getOutput(
|
||||
<String>['status', '--porcelain'],
|
||||
'check that the git checkout is clean',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
return output == '';
|
||||
}
|
||||
|
||||
/// Fetch all branches and associated commits and tags from [remoteName].
|
||||
void fetch(String remoteName) {
|
||||
git.run(
|
||||
<String>['fetch', remoteName, '--tags'],
|
||||
'fetch $remoteName --tags',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtain the version tag of the previous dev release.
|
||||
String getFullTag(String remoteName) {
|
||||
const String glob = '*.*.*-*.*.pre';
|
||||
// describe the latest dev release
|
||||
final String ref = 'refs/remotes/$remoteName/dev';
|
||||
return git.getOutput(
|
||||
<String>['describe', '--match', glob, '--exact-match', '--tags', ref],
|
||||
'obtain last released version number',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
/// Look up the commit for [ref].
|
||||
String reverseParse(String ref) {
|
||||
final String revisionHash = git.getOutput(
|
||||
<String>['rev-parse', ref],
|
||||
'look up the commit for the ref $ref',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
assert(revisionHash.isNotEmpty);
|
||||
return revisionHash;
|
||||
}
|
||||
|
||||
/// Determines if one ref is an ancestor for another.
|
||||
bool isAncestor(String possibleAncestor, String possibleDescendant) {
|
||||
final int exitcode = git.run(
|
||||
<String>[
|
||||
'merge-base',
|
||||
'--is-ancestor',
|
||||
possibleDescendant,
|
||||
possibleAncestor
|
||||
],
|
||||
'verify $possibleAncestor is a direct ancestor of $possibleDescendant.',
|
||||
allowNonZeroExitCode: true,
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
return exitcode == 0;
|
||||
}
|
||||
|
||||
/// Determines if a given commit has a tag.
|
||||
bool isCommitTagged(String commit) {
|
||||
final int exitcode = git.run(
|
||||
<String>['describe', '--exact-match', '--tags', commit],
|
||||
'verify $commit is already tagged',
|
||||
allowNonZeroExitCode: true,
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
return exitcode == 0;
|
||||
}
|
||||
|
||||
/// Resets repository HEAD to [commit].
|
||||
void reset(String commit) {
|
||||
git.run(
|
||||
<String>['reset', commit, '--hard'],
|
||||
'reset to the release commit',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
/// Tag [commit] and push the tag to the remote.
|
||||
void tag(String commit, String tagName, String remote) {
|
||||
git.run(
|
||||
<String>['tag', tagName, commit],
|
||||
'tag the commit with the version label',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
git.run(
|
||||
<String>['push', remote, tagName],
|
||||
'publish the tag to the repo',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
/// Push [commit] to the release channel [branch].
|
||||
void updateChannel(
|
||||
String commit,
|
||||
String remote,
|
||||
String branch, {
|
||||
bool force = false,
|
||||
}) {
|
||||
git.run(
|
||||
<String>[
|
||||
'push',
|
||||
if (force) '--force',
|
||||
remote,
|
||||
'$commit:$branch',
|
||||
],
|
||||
'update the release branch with the commit',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
}
|
||||
|
||||
Version flutterVersion() {
|
||||
// Build tool
|
||||
processManager.runSync(<String>[
|
||||
fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
|
||||
'help',
|
||||
]);
|
||||
// Check version
|
||||
final io.ProcessResult result = processManager.runSync(<String>[
|
||||
fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
|
||||
'--version',
|
||||
'--machine',
|
||||
]);
|
||||
final Map<String, dynamic> versionJson = jsonDecode(
|
||||
globals.stdoutToString(result.stdout),
|
||||
) as Map<String, dynamic>;
|
||||
return Version.fromString(versionJson['frameworkVersion'] as String);
|
||||
}
|
||||
|
||||
/// Create an empty commit and return the revision.
|
||||
@visibleForTesting
|
||||
String authorEmptyCommit([String message = 'An empty commit']) {
|
||||
git.run(
|
||||
<String>[
|
||||
'-c',
|
||||
'user.name=Conductor',
|
||||
'-c',
|
||||
'user.email=conductor@flutter.dev',
|
||||
'commit',
|
||||
'--allow-empty',
|
||||
'-m',
|
||||
'\'$message\'',
|
||||
],
|
||||
'create an empty commit',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
return reverseParse('HEAD');
|
||||
}
|
||||
|
||||
/// Create a new clone of the current repository.
|
||||
///
|
||||
/// The returned repository will inherit all properties from this one, except
|
||||
/// for the upstream, which will be the path to this repository on disk.
|
||||
///
|
||||
/// This method is for testing purposes.
|
||||
@visibleForTesting
|
||||
Repository cloneRepository(String cloneName) {
|
||||
assert(localUpstream);
|
||||
cloneName ??= 'clone-of-$name';
|
||||
return Repository(
|
||||
fileSystem: fileSystem,
|
||||
name: cloneName,
|
||||
parentDirectory: parentDirectory,
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
stdio: stdio,
|
||||
upstream: 'file://${checkoutDirectory.path}/',
|
||||
useExistingCheckout: useExistingCheckout,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum of all the repositories that the Conductor supports.
|
||||
enum RepositoryType {
|
||||
framework,
|
||||
engine,
|
||||
}
|
||||
|
||||
class Checkouts {
|
||||
Checkouts({
|
||||
@required Platform platform,
|
||||
@required this.fileSystem,
|
||||
@required this.processManager,
|
||||
Directory parentDirectory,
|
||||
String directoryName = 'checkouts',
|
||||
}) {
|
||||
if (parentDirectory != null) {
|
||||
directory = parentDirectory.childDirectory(directoryName);
|
||||
} else {
|
||||
String filePath;
|
||||
// If a test
|
||||
if (platform.script.scheme == 'data') {
|
||||
final RegExp pattern = RegExp(
|
||||
r'(file:\/\/[^"]*[/\\]dev\/tools[/\\][^"]+\.dart)',
|
||||
multiLine: true,
|
||||
);
|
||||
final Match match =
|
||||
pattern.firstMatch(Uri.decodeFull(platform.script.path));
|
||||
if (match == null) {
|
||||
throw Exception(
|
||||
'Cannot determine path of script!\n${platform.script.path}',
|
||||
);
|
||||
}
|
||||
filePath = Uri.parse(match.group(1)).path.replaceAll(r'%20', ' ');
|
||||
} else {
|
||||
filePath = platform.script.toFilePath();
|
||||
}
|
||||
final String checkoutsDirname = fileSystem.path.normalize(
|
||||
fileSystem.path.join(
|
||||
fileSystem.path.dirname(filePath),
|
||||
'..',
|
||||
'checkouts',
|
||||
),
|
||||
);
|
||||
directory = fileSystem.directory(checkoutsDirname);
|
||||
}
|
||||
if (!directory.existsSync()) {
|
||||
directory.createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Directory directory;
|
||||
final FileSystem fileSystem;
|
||||
final ProcessManager processManager;
|
||||
|
||||
Repository addRepo({
|
||||
@required RepositoryType repoType,
|
||||
@required Stdio stdio,
|
||||
@required Platform platform,
|
||||
FileSystem fileSystem,
|
||||
String upstream,
|
||||
String name,
|
||||
bool localUpstream = false,
|
||||
bool useExistingCheckout = false,
|
||||
}) {
|
||||
switch (repoType) {
|
||||
case RepositoryType.framework:
|
||||
name ??= 'framework';
|
||||
upstream ??= 'https://github.com/flutter/flutter.git';
|
||||
break;
|
||||
case RepositoryType.engine:
|
||||
name ??= 'engine';
|
||||
upstream ??= 'https://github.com/flutter/engine.git';
|
||||
break;
|
||||
}
|
||||
return Repository(
|
||||
name: name,
|
||||
upstream: upstream,
|
||||
stdio: stdio,
|
||||
platform: platform,
|
||||
fileSystem: fileSystem,
|
||||
parentDirectory: directory,
|
||||
processManager: processManager,
|
||||
localUpstream: localUpstream,
|
||||
useExistingCheckout: useExistingCheckout,
|
||||
);
|
||||
}
|
||||
}
|
@ -2,319 +2,190 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Rolls the dev channel.
|
||||
// Only tested on Linux.
|
||||
//
|
||||
// See: https://github.com/flutter/flutter/wiki/Release-process
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
const String kIncrement = 'increment';
|
||||
const String kX = 'x';
|
||||
const String kY = 'y';
|
||||
const String kZ = 'z';
|
||||
const String kCommit = 'commit';
|
||||
const String kOrigin = 'origin';
|
||||
const String kJustPrint = 'just-print';
|
||||
const String kYes = 'yes';
|
||||
const String kHelp = 'help';
|
||||
const String kForce = 'force';
|
||||
const String kSkipTagging = 'skip-tagging';
|
||||
import './globals.dart';
|
||||
import './repository.dart';
|
||||
import './stdio.dart';
|
||||
import './version.dart';
|
||||
|
||||
const String kUpstreamRemote = 'git@github.com:flutter/flutter.git';
|
||||
|
||||
void main(List<String> args) {
|
||||
final ArgParser argParser = ArgParser(allowTrailingOptions: false);
|
||||
|
||||
ArgResults argResults;
|
||||
try {
|
||||
argResults = parseArguments(argParser, args);
|
||||
} on ArgParserException catch (error) {
|
||||
print(error.message);
|
||||
print(argParser.usage);
|
||||
exit(1);
|
||||
/// Create a new dev release without cherry picks.
|
||||
class RollDev extends Command<void> {
|
||||
RollDev({
|
||||
this.fileSystem,
|
||||
this.platform,
|
||||
this.repository,
|
||||
this.stdio,
|
||||
}) {
|
||||
argParser.addOption(
|
||||
kIncrement,
|
||||
help: 'Specifies which part of the x.y.z version number to increment. Required.',
|
||||
valueHelp: 'level',
|
||||
allowed: <String>['y', 'z', 'm'],
|
||||
allowedHelp: <String, String>{
|
||||
'y': 'Indicates the first dev release after a beta release.',
|
||||
'z': 'Indicates a hotfix to a stable release.',
|
||||
'm': 'Indicates a standard dev release.',
|
||||
},
|
||||
);
|
||||
argParser.addOption(
|
||||
kCommit,
|
||||
help: 'Specifies which git commit to roll to the dev branch. Required.',
|
||||
valueHelp: 'hash',
|
||||
defaultsTo: null, // This option is required
|
||||
);
|
||||
argParser.addFlag(
|
||||
kForce,
|
||||
abbr: 'f',
|
||||
help: 'Force push. Necessary when the previous release had cherry-picks.',
|
||||
negatable: false,
|
||||
);
|
||||
argParser.addFlag(
|
||||
kJustPrint,
|
||||
negatable: false,
|
||||
help:
|
||||
"Don't actually roll the dev channel; "
|
||||
'just print the would-be version and quit.',
|
||||
);
|
||||
argParser.addFlag(
|
||||
kSkipTagging,
|
||||
negatable: false,
|
||||
help: 'Do not create tag and push to remote, only update release branch. '
|
||||
'For recovering when the script fails trying to git push to the release branch.'
|
||||
);
|
||||
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
|
||||
}
|
||||
|
||||
try {
|
||||
run(
|
||||
usage: argParser.usage,
|
||||
final FileSystem fileSystem;
|
||||
final Platform platform;
|
||||
final Stdio stdio;
|
||||
final Repository repository;
|
||||
|
||||
@override
|
||||
String get name => 'roll-dev';
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
'For publishing a dev release without cherry picks.';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
rollDev(
|
||||
argResults: argResults,
|
||||
git: const Git(),
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
repository: repository,
|
||||
stdio: stdio,
|
||||
usage: argParser.usage,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
print(e.toString());
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main script execution.
|
||||
///
|
||||
/// Returns true if publishing was successful, else false.
|
||||
bool run({
|
||||
@visibleForTesting
|
||||
bool rollDev({
|
||||
@required String usage,
|
||||
@required ArgResults argResults,
|
||||
@required Git git,
|
||||
@required Stdio stdio,
|
||||
@required Platform platform,
|
||||
@required FileSystem fileSystem,
|
||||
@required Repository repository,
|
||||
String remoteName = 'origin',
|
||||
}) {
|
||||
final String level = argResults[kIncrement] as String;
|
||||
final String commit = argResults[kCommit] as String;
|
||||
final String origin = argResults[kOrigin] as String;
|
||||
final bool justPrint = argResults[kJustPrint] as bool;
|
||||
final bool autoApprove = argResults[kYes] as bool;
|
||||
final bool help = argResults[kHelp] as bool;
|
||||
final bool force = argResults[kForce] as bool;
|
||||
final bool skipTagging = argResults[kSkipTagging] as bool;
|
||||
|
||||
if (help || level == null || commit == null) {
|
||||
print(
|
||||
'roll_dev.dart --increment=level --commit=hash • update the version tags '
|
||||
'and roll a new dev build.\n$usage'
|
||||
);
|
||||
if (level == null || commit == null) {
|
||||
stdio.printStatus(
|
||||
'roll_dev.dart --increment=level --commit=hash • update the version tags '
|
||||
'and roll a new dev build.\n$usage');
|
||||
return false;
|
||||
}
|
||||
|
||||
final String remote = git.getOutput(
|
||||
'remote get-url $origin',
|
||||
'check whether this is a flutter checkout',
|
||||
);
|
||||
if (remote != kUpstreamRemote) {
|
||||
final String remoteUrl = repository.remoteUrl(remoteName);
|
||||
|
||||
if (!repository.gitCheckoutClean()) {
|
||||
throw Exception(
|
||||
'The remote named $origin is set to $remote, when $kUpstreamRemote was '
|
||||
'expected.\nFor more details see: '
|
||||
'https://github.com/flutter/flutter/wiki/Release-process'
|
||||
);
|
||||
'Your git repository is not clean. Try running "git clean -fd". Warning, '
|
||||
'this will delete files! Run with -n to find out which ones.');
|
||||
}
|
||||
|
||||
if (git.getOutput('status --porcelain', 'check status of your local checkout') != '') {
|
||||
repository.fetch(remoteName);
|
||||
|
||||
// Verify [commit] is valid
|
||||
repository.reverseParse(commit);
|
||||
|
||||
stdio.printStatus('remoteName is $remoteName');
|
||||
final Version lastVersion =
|
||||
Version.fromString(repository.getFullTag(remoteName));
|
||||
|
||||
final Version version =
|
||||
skipTagging ? lastVersion : Version.increment(lastVersion, level);
|
||||
final String tagName = version.toString();
|
||||
|
||||
if (repository.reverseParse(lastVersion.toString()).contains(commit.trim())) {
|
||||
throw Exception(
|
||||
'Your git repository is not clean. Try running "git clean -fd". Warning, '
|
||||
'this will delete files! Run with -n to find out which ones.'
|
||||
);
|
||||
}
|
||||
|
||||
git.run('fetch $origin', 'fetch $origin');
|
||||
|
||||
final String lastVersion = getFullTag(git, origin);
|
||||
|
||||
final String version = skipTagging
|
||||
? lastVersion
|
||||
: incrementLevel(lastVersion, level);
|
||||
|
||||
if (git.getOutput(
|
||||
'rev-parse $lastVersion',
|
||||
'check if commit is already on dev',
|
||||
).contains(commit.trim())) {
|
||||
throw Exception('Commit $commit is already on the dev branch as $lastVersion.');
|
||||
'Commit $commit is already on the dev branch as $lastVersion.');
|
||||
}
|
||||
|
||||
if (justPrint) {
|
||||
print(version);
|
||||
stdio.printStatus(tagName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipTagging) {
|
||||
git.run(
|
||||
'describe --exact-match --tags $commit',
|
||||
'verify $commit is already tagged. You can only use the flag '
|
||||
'`$kSkipTagging` if the commit has already been tagged.'
|
||||
);
|
||||
if (skipTagging && !repository.isCommitTagged(commit)) {
|
||||
throw Exception(
|
||||
'The $kSkipTagging flag is only supported for tagged commits.');
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
git.run(
|
||||
'merge-base --is-ancestor $lastVersion $commit',
|
||||
'verify $lastVersion is a direct ancestor of $commit. The flag `$kForce`'
|
||||
'is required to force push a new release past a cherry-pick',
|
||||
);
|
||||
if (!force && !repository.isAncestor(commit, lastVersion.toString())) {
|
||||
throw Exception(
|
||||
'The previous dev tag $lastVersion is not a direct ancestor of $commit.\n'
|
||||
'The flag "$kForce" is required to force push a new release past a cherry-pick.');
|
||||
}
|
||||
|
||||
git.run('reset $commit --hard', 'reset to the release commit');
|
||||
final String hash = repository.reverseParse(commit);
|
||||
|
||||
final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit');
|
||||
// [commit] can be a prefix for [hash].
|
||||
assert(hash.startsWith(commit));
|
||||
|
||||
// PROMPT
|
||||
|
||||
if (autoApprove) {
|
||||
print('Publishing Flutter $version (${hash.substring(0, 10)}) to the "dev" channel.');
|
||||
stdio.printStatus(
|
||||
'Publishing Flutter $version ($hash) to the "dev" channel.');
|
||||
} else {
|
||||
print('Your tree is ready to publish Flutter $version (${hash.substring(0, 10)}) '
|
||||
'to the "dev" channel.');
|
||||
stdout.write('Are you? [yes/no] ');
|
||||
if (stdin.readLineSync() != 'yes') {
|
||||
print('The dev roll has been aborted.');
|
||||
stdio.printStatus('Your tree is ready to publish Flutter $version '
|
||||
'($hash) to the "dev" channel.');
|
||||
stdio.write('Are you? [yes/no] ');
|
||||
if (stdio.readLineSync() != 'yes') {
|
||||
stdio.printError('The dev roll has been aborted.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipTagging) {
|
||||
git.run('tag $version', 'tag the commit with the version label');
|
||||
git.run('push $origin $version', 'publish the version');
|
||||
repository.tag(commit, version.toString(), remoteName);
|
||||
}
|
||||
git.run(
|
||||
'push ${force ? "--force " : ""}$origin HEAD:dev',
|
||||
'land the new version on the "dev" branch',
|
||||
|
||||
repository.updateChannel(
|
||||
commit,
|
||||
remoteName,
|
||||
'dev',
|
||||
force: force,
|
||||
);
|
||||
|
||||
stdio.printStatus(
|
||||
'Flutter version $version has been rolled to the "dev" channel at $remoteUrl.',
|
||||
);
|
||||
print('Flutter version $version has been rolled to the "dev" channel!');
|
||||
return true;
|
||||
}
|
||||
|
||||
ArgResults parseArguments(ArgParser argParser, List<String> args) {
|
||||
argParser.addOption(
|
||||
kIncrement,
|
||||
help: 'Specifies which part of the x.y.z version number to increment. Required.',
|
||||
valueHelp: 'level',
|
||||
allowed: <String>[kX, kY, kZ],
|
||||
allowedHelp: <String, String>{
|
||||
kX: 'Indicates a major development, e.g. typically changed after a big press event.',
|
||||
kY: 'Indicates a minor development, e.g. typically changed after a beta release.',
|
||||
kZ: 'Indicates the least notable level of change. You normally want this.',
|
||||
},
|
||||
);
|
||||
argParser.addOption(
|
||||
kCommit,
|
||||
help: 'Specifies which git commit to roll to the dev branch. Required.',
|
||||
valueHelp: 'hash',
|
||||
defaultsTo: null, // This option is required
|
||||
);
|
||||
argParser.addOption(
|
||||
kOrigin,
|
||||
help: 'Specifies the name of the upstream repository',
|
||||
valueHelp: 'repository',
|
||||
defaultsTo: 'upstream',
|
||||
);
|
||||
argParser.addFlag(
|
||||
kForce,
|
||||
abbr: 'f',
|
||||
help: 'Force push. Necessary when the previous release had cherry-picks.',
|
||||
negatable: false,
|
||||
);
|
||||
argParser.addFlag(
|
||||
kJustPrint,
|
||||
negatable: false,
|
||||
help:
|
||||
"Don't actually roll the dev channel; "
|
||||
'just print the would-be version and quit.',
|
||||
);
|
||||
argParser.addFlag(
|
||||
kSkipTagging,
|
||||
negatable: false,
|
||||
help: 'Do not create tag and push to remote, only update release branch. '
|
||||
'For recovering when the script fails trying to git push to the release branch.'
|
||||
);
|
||||
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
|
||||
argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
|
||||
|
||||
return argParser.parse(args);
|
||||
}
|
||||
|
||||
/// Obtain the version tag of the previous dev release.
|
||||
String getFullTag(Git git, String remote) {
|
||||
const String glob = '*.*.*-*.*.pre';
|
||||
// describe the latest dev release
|
||||
final String ref = 'refs/remotes/$remote/dev';
|
||||
return git.getOutput(
|
||||
'describe --match $glob --exact-match --tags $ref',
|
||||
'obtain last released version number',
|
||||
);
|
||||
}
|
||||
|
||||
Match parseFullTag(String version) {
|
||||
// of the form: x.y.z-m.n.pre
|
||||
final RegExp versionPattern = RegExp(
|
||||
r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$');
|
||||
return versionPattern.matchAsPrefix(version);
|
||||
}
|
||||
|
||||
String getVersionFromParts(List<int> parts) {
|
||||
// where parts correspond to [x, y, z, m, n] from tag
|
||||
assert(parts.length == 5);
|
||||
final StringBuffer buf = StringBuffer()
|
||||
// take x, y, and z
|
||||
..write(parts.take(3).join('.'))
|
||||
..write('-')
|
||||
// skip x, y, and z, take m and n
|
||||
..write(parts.skip(3).take(2).join('.'))
|
||||
..write('.pre');
|
||||
// return a string that looks like: '1.2.3-4.5.pre'
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/// A wrapper around git process calls that can be mocked for unit testing.
|
||||
class Git {
|
||||
const Git();
|
||||
|
||||
String getOutput(String command, String explanation) {
|
||||
final ProcessResult result = _run(command);
|
||||
if ((result.stderr as String).isEmpty && result.exitCode == 0)
|
||||
return (result.stdout as String).trim();
|
||||
_reportFailureAndExit(result, explanation);
|
||||
return null; // for the analyzer's sake
|
||||
}
|
||||
|
||||
void run(String command, String explanation) {
|
||||
final ProcessResult result = _run(command);
|
||||
if (result.exitCode != 0)
|
||||
_reportFailureAndExit(result, explanation);
|
||||
}
|
||||
|
||||
ProcessResult _run(String command) {
|
||||
return Process.runSync('git', command.split(' '));
|
||||
}
|
||||
|
||||
void _reportFailureAndExit(ProcessResult result, String explanation) {
|
||||
final StringBuffer message = StringBuffer();
|
||||
if (result.exitCode != 0) {
|
||||
message.writeln('Failed to $explanation. Git exited with error code ${result.exitCode}.');
|
||||
} else {
|
||||
message.writeln('Failed to $explanation.');
|
||||
}
|
||||
if ((result.stdout as String).isNotEmpty)
|
||||
message.writeln('stdout from git:\n${result.stdout}\n');
|
||||
if ((result.stderr as String).isNotEmpty)
|
||||
message.writeln('stderr from git:\n${result.stderr}\n');
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a copy of the [version] with [level] incremented by one.
|
||||
String incrementLevel(String version, String level) {
|
||||
final Match match = parseFullTag(version);
|
||||
if (match == null) {
|
||||
String errorMessage;
|
||||
if (version.isEmpty) {
|
||||
errorMessage = 'Could not determine the version for this build.';
|
||||
} else {
|
||||
errorMessage = 'Git reported the latest version as "$version", which '
|
||||
'does not fit the expected pattern.';
|
||||
}
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
final List<int> parts = match.groups(<int>[1, 2, 3, 4, 5]).map<int>(int.parse).toList();
|
||||
|
||||
switch (level) {
|
||||
case kX:
|
||||
parts[0] += 1;
|
||||
parts[1] = 0;
|
||||
parts[2] = 0;
|
||||
parts[3] = 0;
|
||||
parts[4] = 0;
|
||||
break;
|
||||
case kY:
|
||||
parts[1] += 1;
|
||||
parts[2] = 0;
|
||||
parts[3] = 0;
|
||||
parts[4] = 0;
|
||||
break;
|
||||
case kZ:
|
||||
parts[2] = 0;
|
||||
parts[3] += 1;
|
||||
parts[4] = 0;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
|
||||
}
|
||||
return getVersionFromParts(parts);
|
||||
}
|
||||
|
62
dev/tools/lib/stdio.dart
Normal file
62
dev/tools/lib/stdio.dart
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
abstract class Stdio {
|
||||
/// Error/warning messages printed to STDERR.
|
||||
void printError(String message);
|
||||
|
||||
/// Ordinary STDOUT messages.
|
||||
void printStatus(String message);
|
||||
|
||||
/// Debug messages that are only printed in verbose mode.
|
||||
void printTrace(String message);
|
||||
|
||||
/// Write string to STDOUT without trailing newline.
|
||||
void write(String message);
|
||||
|
||||
/// Read a line of text from STDIN.
|
||||
String readLineSync();
|
||||
}
|
||||
|
||||
/// A logger that will print out trace messages.
|
||||
class VerboseStdio extends Stdio {
|
||||
VerboseStdio({
|
||||
@required this.stdout,
|
||||
@required this.stderr,
|
||||
@required this.stdin,
|
||||
}) : assert(stdout != null), assert(stderr != null), assert(stdin != null);
|
||||
|
||||
final Stdout stdout;
|
||||
final Stdout stderr;
|
||||
final Stdin stdin;
|
||||
|
||||
@override
|
||||
void printError(String message) {
|
||||
stderr.writeln(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void printStatus(String message) {
|
||||
stdout.writeln(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTrace(String message) {
|
||||
stdout.writeln(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String message) {
|
||||
stdout.write(message);
|
||||
}
|
||||
|
||||
@override
|
||||
String readLineSync() {
|
||||
return stdin.readLineSync();
|
||||
}
|
||||
}
|
211
dev/tools/lib/version.dart
Normal file
211
dev/tools/lib/version.dart
Normal file
@ -0,0 +1,211 @@
|
||||
// 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 'package:meta/meta.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,
|
||||
}
|
||||
|
||||
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+)$'),
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(int.parse).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(int.parse).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(int.parse).toList();
|
||||
return Version(
|
||||
x: parts[0],
|
||||
y: parts[1],
|
||||
z: parts[2],
|
||||
m: parts[3],
|
||||
n: parts[4],
|
||||
commits: parts[5],
|
||||
type: VersionType.latest,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
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.');
|
||||
break;
|
||||
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':
|
||||
// Regular dev release.
|
||||
assert(previousVersion.type == VersionType.development);
|
||||
assert(nextM != null);
|
||||
nextM += 1;
|
||||
nextN = 0;
|
||||
break;
|
||||
case 'n':
|
||||
// Hotfix to internal roll.
|
||||
nextN += 1;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown increment level $increment.');
|
||||
}
|
||||
return Version(
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
z: nextZ,
|
||||
m: nextM,
|
||||
n: nextN,
|
||||
type: nextVersionType,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
final int z;
|
||||
|
||||
/// Zero-indexed count of dev releases after a beta release.
|
||||
final int m;
|
||||
|
||||
/// Number of hotfixes required to make a dev release.
|
||||
final int n;
|
||||
|
||||
/// Number of commits past last tagged dev release.
|
||||
final int commits;
|
||||
|
||||
final VersionType 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';
|
||||
}
|
||||
return null; // For analyzer
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ environment:
|
||||
dependencies:
|
||||
archive: 2.0.13
|
||||
args: 1.6.0
|
||||
flutter_tools:
|
||||
path: '../../packages/flutter_tools'
|
||||
http: 0.12.2
|
||||
intl: 0.16.1
|
||||
meta: 1.3.0-nullsafety.6
|
||||
|
@ -7,6 +7,10 @@ import 'dart:io';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
import 'package:test/test.dart' as test_package show TypeMatcher;
|
||||
|
||||
import 'package:dev_tools/stdio.dart';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
|
||||
export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
// Defines a 'package:test' shim.
|
||||
@ -25,3 +29,111 @@ void tryToDelete(Directory directory) {
|
||||
print('Failed to delete ${directory.path}: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Matcher throwsExceptionWith(String messageSubString) {
|
||||
return throwsA(
|
||||
isA<Exception>().having(
|
||||
(Exception e) => e.toString(),
|
||||
'description',
|
||||
contains(messageSubString),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class TestStdio implements Stdio {
|
||||
TestStdio({
|
||||
this.verbose = false,
|
||||
List<String> stdin,
|
||||
}) {
|
||||
_stdin = stdin ?? <String>[];
|
||||
}
|
||||
|
||||
final StringBuffer _error = StringBuffer();
|
||||
String get error => _error.toString();
|
||||
|
||||
final StringBuffer _stdout = StringBuffer();
|
||||
String get stdout => _stdout.toString();
|
||||
final bool verbose;
|
||||
List<String> _stdin;
|
||||
|
||||
@override
|
||||
void printError(String message) {
|
||||
_error.writeln(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void printStatus(String message) {
|
||||
_stdout.writeln(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTrace(String message) {
|
||||
if (verbose) {
|
||||
_stdout.writeln(message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String message) {
|
||||
_stdout.write(message);
|
||||
}
|
||||
|
||||
@override
|
||||
String readLineSync() {
|
||||
if (_stdin.isEmpty) {
|
||||
throw Exception('Unexpected call to readLineSync!');
|
||||
}
|
||||
return _stdin.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeArgResults implements ArgResults {
|
||||
FakeArgResults({
|
||||
String level,
|
||||
String commit,
|
||||
String remote,
|
||||
bool justPrint = false,
|
||||
bool autoApprove = true, // so we don't have to mock stdin
|
||||
bool help = false,
|
||||
bool force = false,
|
||||
bool skipTagging = false,
|
||||
}) : _parsedArgs = <String, dynamic>{
|
||||
'increment': level,
|
||||
'commit': commit,
|
||||
'remote': remote,
|
||||
'just-print': justPrint,
|
||||
'yes': autoApprove,
|
||||
'help': help,
|
||||
'force': force,
|
||||
'skip-tagging': skipTagging,
|
||||
};
|
||||
|
||||
@override
|
||||
String name;
|
||||
|
||||
@override
|
||||
ArgResults command;
|
||||
|
||||
@override
|
||||
final List<String> rest = <String>[];
|
||||
|
||||
@override
|
||||
List<String> arguments;
|
||||
|
||||
final Map<String, dynamic> _parsedArgs;
|
||||
|
||||
@override
|
||||
Iterable<String> get options {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic operator [](String name) {
|
||||
return _parsedArgs[name];
|
||||
}
|
||||
|
||||
@override
|
||||
bool wasParsed(String name) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
132
dev/tools/test/roll_dev_integration_test.dart
Normal file
132
dev/tools/test/roll_dev_integration_test.dart
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import 'package:dev_tools/roll_dev.dart' show rollDev;
|
||||
import 'package:dev_tools/repository.dart';
|
||||
import 'package:dev_tools/version.dart';
|
||||
|
||||
import './common.dart';
|
||||
|
||||
void main() {
|
||||
group('roll-dev', () {
|
||||
TestStdio stdio;
|
||||
Platform platform;
|
||||
ProcessManager processManager;
|
||||
FileSystem fileSystem;
|
||||
const String usageString = 'Usage: flutter conductor.';
|
||||
|
||||
Checkouts checkouts;
|
||||
Repository frameworkUpstream;
|
||||
Repository framework;
|
||||
|
||||
setUp(() {
|
||||
platform = const LocalPlatform();
|
||||
fileSystem = const LocalFileSystem();
|
||||
processManager = const LocalProcessManager();
|
||||
stdio = TestStdio(verbose: true);
|
||||
checkouts = Checkouts(
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
);
|
||||
|
||||
frameworkUpstream = checkouts.addRepo(
|
||||
repoType: RepositoryType.framework,
|
||||
name: 'framework-upstream',
|
||||
stdio: stdio,
|
||||
platform: platform,
|
||||
localUpstream: true,
|
||||
fileSystem: fileSystem,
|
||||
useExistingCheckout: false,
|
||||
);
|
||||
|
||||
// This repository has [frameworkUpstream] set as its push/pull remote.
|
||||
framework = frameworkUpstream.cloneRepository('test-framework');
|
||||
});
|
||||
|
||||
test('increment m', () {
|
||||
final Version initialVersion = framework.flutterVersion();
|
||||
|
||||
final String latestCommit = framework.authorEmptyCommit();
|
||||
|
||||
final FakeArgResults fakeArgResults = FakeArgResults(
|
||||
level: 'm',
|
||||
commit: latestCommit,
|
||||
remote: 'origin',
|
||||
);
|
||||
|
||||
expect(
|
||||
rollDev(
|
||||
usage: usageString,
|
||||
argResults: fakeArgResults,
|
||||
stdio: stdio,
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
repository: framework,
|
||||
),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
stdio.stdout,
|
||||
contains(RegExp(r'Publishing Flutter \d+\.\d+\.\d+-\d+\.\d+\.pre \(')),
|
||||
);
|
||||
|
||||
final Version finalVersion = framework.flutterVersion();
|
||||
expect(
|
||||
initialVersion.toString() != finalVersion.toString(),
|
||||
true,
|
||||
reason: 'initialVersion = $initialVersion; finalVersion = $finalVersion',
|
||||
);
|
||||
expect(finalVersion.n, 0);
|
||||
expect(finalVersion.commits, null);
|
||||
});
|
||||
|
||||
test('increment y', () {
|
||||
final Version initialVersion = framework.flutterVersion();
|
||||
|
||||
final String latestCommit = framework.authorEmptyCommit();
|
||||
|
||||
final FakeArgResults fakeArgResults = FakeArgResults(
|
||||
level: 'y',
|
||||
commit: latestCommit,
|
||||
remote: 'origin',
|
||||
);
|
||||
|
||||
expect(
|
||||
rollDev(
|
||||
usage: usageString,
|
||||
argResults: fakeArgResults,
|
||||
stdio: stdio,
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
repository: framework,
|
||||
),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
stdio.stdout,
|
||||
contains(RegExp(r'Publishing Flutter \d+\.\d+\.\d+-\d+\.\d+\.pre \(')),
|
||||
);
|
||||
|
||||
final Version finalVersion = framework.flutterVersion();
|
||||
expect(
|
||||
initialVersion.toString() != finalVersion.toString(),
|
||||
true,
|
||||
reason: 'initialVersion = $initialVersion; finalVersion = $finalVersion',
|
||||
);
|
||||
expect(finalVersion.y, initialVersion.y + 1);
|
||||
expect(finalVersion.z, 0);
|
||||
expect(finalVersion.m, 0);
|
||||
expect(finalVersion.n, 0);
|
||||
expect(finalVersion.commits, null);
|
||||
});
|
||||
}, onPlatform: <String, dynamic>{
|
||||
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
87
dev/tools/test/version_test.dart
Normal file
87
dev/tools/test/version_test.dart
Normal file
@ -0,0 +1,87 @@
|
||||
// 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 'package:dev_tools/version.dart';
|
||||
|
||||
import './common.dart';
|
||||
|
||||
void main() {
|
||||
group('Version.increment()', () {
|
||||
test('throws exception on nonsensical `level`', () {
|
||||
final List<String> levels = <String>['f', '0', 'xyz'];
|
||||
for (final String level in levels) {
|
||||
final Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(
|
||||
() => Version.increment(version, level).toString(),
|
||||
throwsExceptionWith('Unknown increment level $level.'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('does not support incrementing x', () {
|
||||
const String level = 'x';
|
||||
|
||||
final Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(
|
||||
() => Version.increment(version, level).toString(),
|
||||
throwsExceptionWith(
|
||||
'Incrementing $level is not supported by this tool'),
|
||||
);
|
||||
});
|
||||
|
||||
test('successfully increments y', () {
|
||||
const String level = 'y';
|
||||
|
||||
Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.1.0-0.0.pre');
|
||||
|
||||
version = Version.fromString('10.20.0-40.50.pre');
|
||||
expect(Version.increment(version, level).toString(), '10.21.0-0.0.pre');
|
||||
|
||||
version = Version.fromString('1.18.0-3.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.19.0-0.0.pre');
|
||||
});
|
||||
|
||||
test('successfully increments z', () {
|
||||
const String level = 'm';
|
||||
|
||||
Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre');
|
||||
|
||||
version = Version.fromString('10.20.0-40.50.pre');
|
||||
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre');
|
||||
|
||||
version = Version.fromString('1.18.0-3.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre');
|
||||
});
|
||||
|
||||
test('successfully increments m', () {
|
||||
const String level = 'm';
|
||||
|
||||
Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre');
|
||||
|
||||
version = Version.fromString('10.20.0-40.50.pre');
|
||||
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre');
|
||||
|
||||
version = Version.fromString('1.18.0-3.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre');
|
||||
});
|
||||
|
||||
test('successfully increments n', () {
|
||||
const String level = 'n';
|
||||
|
||||
Version version = Version.fromString('1.0.0-0.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.0.0-0.1.pre');
|
||||
|
||||
version = Version.fromString('10.20.0-40.50.pre');
|
||||
expect(Version.increment(version, level).toString(), '10.20.0-40.51.pre');
|
||||
|
||||
version = Version.fromString('1.18.0-3.0.pre');
|
||||
expect(Version.increment(version, level).toString(), '1.18.0-3.1.pre');
|
||||
});
|
||||
}, onPlatform: <String, dynamic>{
|
||||
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user